diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 9be4fc49..80f59570 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -7,12 +7,14 @@ + diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 0583eaef..8106aba8 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,6 +1,8 @@ + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 54089aca..7fed079a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,9 +1,11 @@ + diff --git a/README.md b/README.md index 1bb27b87..514e63ed 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## CMPE352 Fundamentals of Software Engineering 2023 Spring - GROUP 5 +## CMPE451 Project Development in Software Engineering 2023 Fall-GROUP 5 [![Watch on GitHub](https://img.shields.io/github/watchers/bounswe/bounswe2023group5.svg?style=social)](https://github.com/bounswe/bounswe2023group5/watchers) [![Star on GitHub](https://img.shields.io/github/stars/bounswe/bounswe2023group5.svg?style=social)](https://github.com/bounswe/bounswe2023group5/stargazers) diff --git a/app/annotation/annotation/.gitignore b/app/annotation/annotation/.gitignore new file mode 100644 index 00000000..4900fc67 --- /dev/null +++ b/app/annotation/annotation/.gitignore @@ -0,0 +1,40 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +src/main/resources/.env \ No newline at end of file diff --git a/app/annotation/annotation/.idea/.gitignore b/app/annotation/annotation/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/app/annotation/annotation/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/app/annotation/annotation/.idea/encodings.xml b/app/annotation/annotation/.idea/encodings.xml new file mode 100644 index 00000000..aa00ffab --- /dev/null +++ b/app/annotation/annotation/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/annotation/annotation/.idea/misc.xml b/app/annotation/annotation/.idea/misc.xml new file mode 100644 index 00000000..82dbec8a --- /dev/null +++ b/app/annotation/annotation/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/annotation/annotation/.idea/vcs.xml b/app/annotation/annotation/.idea/vcs.xml new file mode 100644 index 00000000..c2365ab1 --- /dev/null +++ b/app/annotation/annotation/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/annotation/annotation/Dockerfile b/app/annotation/annotation/Dockerfile new file mode 100644 index 00000000..9ad98765 --- /dev/null +++ b/app/annotation/annotation/Dockerfile @@ -0,0 +1,6 @@ +FROM maven:3.9.6-eclipse-temurin-21-jammy +WORKDIR /app/annotation +COPY . /app/annotation +RUN mvn clean install +EXPOSE 8080 +ENTRYPOINT ["java","-jar","/app/annotation/target/annotation-1.0-SNAPSHOT.jar"] \ No newline at end of file diff --git a/app/annotation/annotation/pom.xml b/app/annotation/annotation/pom.xml new file mode 100644 index 00000000..42c2a8f3 --- /dev/null +++ b/app/annotation/annotation/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.0.0 + + + com.app.annotation + annotation + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + 1.18.30 + provided + + + org.springframework.boot + spring-boot-maven-plugin + 3.0.0 + + + org.modelmapper + modelmapper + 3.1.1 + + + me.paulschwarz + spring-dotenv + 4.0.0 + + + jakarta.validation + jakarta.validation-api + 3.0.2 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.0.0 + provided + + + io.swagger.core.v3 + swagger-annotations-jakarta + 2.2.7 + compile + + + org.springframework + spring-web + + + org.springframework.security + spring-security-web + + + org.springframework.security + spring-security-config + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/Main.java b/app/annotation/annotation/src/main/java/com/app/annotation/Main.java new file mode 100644 index 00000000..85ebd8e8 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/Main.java @@ -0,0 +1,11 @@ +package com.app.annotation; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} \ No newline at end of file diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/config/CorsConfig.java b/app/annotation/annotation/src/main/java/com/app/annotation/config/CorsConfig.java new file mode 100644 index 00000000..fe18cb27 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/config/CorsConfig.java @@ -0,0 +1,27 @@ +package com.app.annotation.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +@Configuration +public class CorsConfig { + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + final CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173", "http://localhost", "http://ec2-51-20-78-40.eu-north-1.compute.amazonaws.com/")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowCredentials(true); + configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "Set-Cookie", "credentials")); + configuration.setExposedHeaders(Arrays.asList("Set-Cookie")); + + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/config/MapperConfig.java b/app/annotation/annotation/src/main/java/com/app/annotation/config/MapperConfig.java new file mode 100644 index 00000000..2b2c3201 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/config/MapperConfig.java @@ -0,0 +1,17 @@ +package com.app.annotation.config; + +import org.modelmapper.ModelMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MapperConfig { + + @Bean + public static ModelMapper modelMapper() { + ModelMapper modelMapper = new ModelMapper(); + modelMapper.getConfiguration().setFieldMatchingEnabled(true); + return modelMapper; + } + +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/config/SecurityConfig.java b/app/annotation/annotation/src/main/java/com/app/annotation/config/SecurityConfig.java new file mode 100644 index 00000000..58415183 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/config/SecurityConfig.java @@ -0,0 +1,25 @@ +package com.app.annotation.config; + + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .cors() + .and() + .csrf().disable() + ; + + return http.build(); + } + +} \ No newline at end of file diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/config/SpringdocConfig.java b/app/annotation/annotation/src/main/java/com/app/annotation/config/SpringdocConfig.java new file mode 100644 index 00000000..a115ca76 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/config/SpringdocConfig.java @@ -0,0 +1,18 @@ +package com.app.annotation.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@OpenAPIDefinition +@Configuration +public class SpringdocConfig { + + @Bean + public OpenAPI baseOpenAPI() { + return new OpenAPI().info(new Info().title("Spring Doc").version("1.0.0").description("Spring doc")); + } + +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/controller/AnnotationController.java b/app/annotation/annotation/src/main/java/com/app/annotation/controller/AnnotationController.java new file mode 100644 index 00000000..803cd474 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/controller/AnnotationController.java @@ -0,0 +1,62 @@ +package com.app.annotation.controller; + +import com.app.annotation.service.AnnotationService; +import com.app.annotation.dto.request.CreateAnnotationRequestDto; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/annotation") +public class AnnotationController { + + private final AnnotationService annotationService; + + public AnnotationController(AnnotationService annotationService) { + this.annotationService = annotationService; + } + + @PostMapping("/create") + public ResponseEntity> createAnnotation(@RequestBody CreateAnnotationRequestDto dto) { + Map annotation = annotationService.createAnnotation(dto); + return ResponseEntity.ok(annotation); + } + + @DeleteMapping("/delete") + public ResponseEntity deleteAnnotation(@RequestParam String id) { + boolean isDeleted = annotationService.deleteAnnotation(id); + if (!isDeleted) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(true); + } + + @PutMapping("/update") + public ResponseEntity> updateAnnotation(@RequestBody CreateAnnotationRequestDto dto) { + Map annotation = annotationService.updateAnnotation(dto); + if(annotation == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(annotation); + } + + @GetMapping("/get-source-annotations") + public ResponseEntity>> getAnnotation(@RequestParam String source) { + List> annotations = annotationService.getAnnotations(source); + if(annotations == null || annotations.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(annotations); + } + + @GetMapping("/get-image-annotations") + public ResponseEntity>> getImageAnnotations(@RequestParam String source) { + List> annotations = annotationService.getImageAnnotations(source); + if(annotations == null || annotations.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(annotations); + } +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/controller/CustomExceptionHandler.java b/app/annotation/annotation/src/main/java/com/app/annotation/controller/CustomExceptionHandler.java new file mode 100644 index 00000000..af1c47b1 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/controller/CustomExceptionHandler.java @@ -0,0 +1,34 @@ +package com.app.annotation.controller; + +import com.app.annotation.exception.BadRequestException; +import com.app.annotation.exception.ResourceNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ControllerAdvice +public class CustomExceptionHandler { + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleResourceNotFoundException(ResourceNotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage()); + } + + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleBadRequestException(BadRequestException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleValidationException(MethodArgumentNotValidException ex) { + String defaultMessage = ex.getBindingResult() + .getFieldError() + .getDefaultMessage(); + return ResponseEntity.badRequest().body(defaultMessage); + } +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/dto/request/BodyDto.java b/app/annotation/annotation/src/main/java/com/app/annotation/dto/request/BodyDto.java new file mode 100644 index 00000000..3d294f84 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/dto/request/BodyDto.java @@ -0,0 +1,27 @@ +package com.app.annotation.dto.request; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class BodyDto { + + @Pattern(regexp = "TextualBody", message = "Currently only Embedded Textual Bodies are supported.") + private String type; + + private String id; + + private String purpose; + + @NotEmpty(message = "body value can not be empty.") + private String value; + + private String format; + + private String language; +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/dto/request/CreateAnnotationRequestDto.java b/app/annotation/annotation/src/main/java/com/app/annotation/dto/request/CreateAnnotationRequestDto.java new file mode 100644 index 00000000..5a639123 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/dto/request/CreateAnnotationRequestDto.java @@ -0,0 +1,34 @@ +package com.app.annotation.dto.request; + +import com.app.annotation.model.Creator; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CreateAnnotationRequestDto { + + @NotEmpty(message = "annotation target can not be empty.") + private TargetDto target; + + @NotEmpty(message = "annotation body can not be empty.") + private List body; + + @NotEmpty(message = "annotation id can not be empty.") + private String id; + + @NotEmpty(message = "annotation type can not be empty.") + private String type; + + private String motivation; + + private Date created; + + private Creator creator; +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/dto/request/SelectorDto.java b/app/annotation/annotation/src/main/java/com/app/annotation/dto/request/SelectorDto.java new file mode 100644 index 00000000..31de5160 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/dto/request/SelectorDto.java @@ -0,0 +1,35 @@ +package com.app.annotation.dto.request; + +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SelectorDto { + + @Pattern(regexp = "^(TextQuoteSelector|TextPositionSelector)$", + message = "Currently only Text Quote Selector and Text Position Selector are supported.") + private String type; + + private String exact; + + private String prefix; + + private String suffix; + + @PositiveOrZero + private Integer start; + + @Positive + private Integer end; + + private String value; + + @Pattern(regexp = "^(https?|ftp)://[^\\s/$.?#].\\S*$", message = "Invalid URL") + private String conformsTo; +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/dto/request/TargetDto.java b/app/annotation/annotation/src/main/java/com/app/annotation/dto/request/TargetDto.java new file mode 100644 index 00000000..c963f559 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/dto/request/TargetDto.java @@ -0,0 +1,74 @@ +package com.app.annotation.dto.request; + + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSetter; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TargetDto { + + private String id; + + private String type; + + private String format; + + private String textDirection; + + @NotEmpty(message = "target source can not be empty.") + private String source; + + @NotEmpty(message = "target selector can not be empty.") + @JsonIgnore + private List selector; + + @JsonSetter("selector") + public void setSelector(Object selector) { + if (selector instanceof List) { + List> hashList; + hashList = (List>) selector; + this.selector = mapToSelectorDtoList(hashList); + } else { + LinkedHashMap hash = (LinkedHashMap) selector; + this.selector = Collections.singletonList(convertToSelectorDto(hash)); + } + } + + public static List mapToSelectorDtoList(List> inputList) { + List resultList = new ArrayList<>(); + + if (inputList != null) { + for (LinkedHashMap map : inputList) { + SelectorDto selectorDto = convertToSelectorDto(map); + resultList.add(selectorDto); + } + } + + return resultList; + } + + private static SelectorDto convertToSelectorDto(LinkedHashMap map) { + SelectorDto selectorDto = new SelectorDto(); + selectorDto.setType((String) map.get("type")); + selectorDto.setExact((String) map.get("exact")); + selectorDto.setPrefix((String) map.get("prefix")); + selectorDto.setSuffix((String) map.get("suffix")); + selectorDto.setStart((Integer) map.get("start")); + selectorDto.setEnd((Integer) map.get("end")); + selectorDto.setValue((String) map.get("value")); + selectorDto.setConformsTo((String) map.get("conformsTo")); + // Add other fields as needed + + return selectorDto; + } +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/exception/BadRequestException.java b/app/annotation/annotation/src/main/java/com/app/annotation/exception/BadRequestException.java new file mode 100644 index 00000000..47488e85 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/exception/BadRequestException.java @@ -0,0 +1,7 @@ +package com.app.annotation.exception; + +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/exception/ResourceNotFoundException.java b/app/annotation/annotation/src/main/java/com/app/annotation/exception/ResourceNotFoundException.java new file mode 100644 index 00000000..e39a2c98 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/exception/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package com.app.annotation.exception; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/model/Annotation.java b/app/annotation/annotation/src/main/java/com/app/annotation/model/Annotation.java new file mode 100644 index 00000000..290856ec --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/model/Annotation.java @@ -0,0 +1,53 @@ +package com.app.annotation.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.*; + +@Document(collection = "Annotation") +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class Annotation { + + @Id + private String id; + + private Target target; + + private List body; + + private String type; + + private String motivation; + + private Date created; + + private Creator creator; + + public Map toJSON() { + Map json = new HashMap<>(); + json.put("@context", "http://www.w3.org/ns/anno.jsonld"); + if (this.id != null) json.put("id", this.id); + if (this.type != null) json.put("type", this.type); + if (this.motivation != null) json.put("motivation", this.motivation); + if (this.created != null) json.put("created", this.created); + if (this.target != null) json.put("target", this.target.toJSON()); + if (this.body != null) { + List> bodyList = new ArrayList<>(); + + for (Body b : this.body) { + bodyList.add(b.toJSON()); + } + json.put("body", bodyList); + } + if (this.creator != null) json.put("creator", this.creator.toJSON()); + return json; + } +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/model/Body.java b/app/annotation/annotation/src/main/java/com/app/annotation/model/Body.java new file mode 100644 index 00000000..2cf68103 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/model/Body.java @@ -0,0 +1,37 @@ +package com.app.annotation.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Body { + private String type; + + private String id; + + private String purpose; + + private String value; + + private String format; + + private String language; + + public Map toJSON() { + Map json = new HashMap<>(); + if (this.type != null) json.put("type", this.type); + if (this.id != null) json.put("id", this.id); + if (this.purpose != null) json.put("purpose", this.purpose); + if (this.value != null) json.put("value", this.value); + if (this.format != null) json.put("format", this.format); + if (this.language != null) json.put("language", this.language); + + return json; + } +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/model/Creator.java b/app/annotation/annotation/src/main/java/com/app/annotation/model/Creator.java new file mode 100644 index 00000000..7172f164 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/model/Creator.java @@ -0,0 +1,32 @@ +package com.app.annotation.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Creator { + + private String id; + + private String type; + + private String name; + + private String nickname; + + public Map toJSON() { + Map json = new HashMap<>(); + if (this.type != null) json.put("type", this.type); + if (this.id != null) json.put("id", this.id); + if (this.name != null) json.put("name", this.name); + if (this.nickname != null) json.put("nickname", this.nickname); + + return json; + } +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/model/FragmentSelector.java b/app/annotation/annotation/src/main/java/com/app/annotation/model/FragmentSelector.java new file mode 100644 index 00000000..178c9340 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/model/FragmentSelector.java @@ -0,0 +1,27 @@ +package com.app.annotation.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; + +@NoArgsConstructor +@Getter +@Setter +public class FragmentSelector extends Selector { + + private String value; + + private String conformsTo; + + @Override + public Map toJSON() { + Map json = new HashMap<>(); + if (this.type != null) json.put("type", this.type); + if (this.value != null) json.put("value", this.value); + if (this.conformsTo != null) json.put("conformsTo", this.conformsTo); + return json; + } +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/model/Selector.java b/app/annotation/annotation/src/main/java/com/app/annotation/model/Selector.java new file mode 100644 index 00000000..3317da4e --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/model/Selector.java @@ -0,0 +1,15 @@ +package com.app.annotation.model; + +import lombok.*; + +import java.util.Map; + +@Getter +@Setter +public abstract class Selector { + + String type; + + public abstract Map toJSON(); + +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/model/Target.java b/app/annotation/annotation/src/main/java/com/app/annotation/model/Target.java new file mode 100644 index 00000000..0a0591f5 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/model/Target.java @@ -0,0 +1,49 @@ +package com.app.annotation.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class Target { + private String id; + + private String type; + + private String format; + + private String textDirection; + + private String source; + + private List selector; + + public Map toJSON() { + Map json = new HashMap<>(); + if (this.id != null) json.put("id", this.id); + if (this.type != null) json.put("type", this.type); + if (this.format != null) json.put("format", this.format); + if (this.textDirection != null) json.put("textDirection", this.textDirection); + if (this.source != null) json.put("source", this.source); + if (this.selector != null && !this.selector.isEmpty()) { + if (this.selector.size() > 1) { + List> selectorList = new ArrayList<>(); + + for (Selector s : this.selector) { + selectorList.add(s.toJSON()); + } + json.put("selector", selectorList); + } else { + json.put("selector", this.selector.get(0).toJSON()); + } + } + return json; + } +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/model/TextPositionSelector.java b/app/annotation/annotation/src/main/java/com/app/annotation/model/TextPositionSelector.java new file mode 100644 index 00000000..8dc1e918 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/model/TextPositionSelector.java @@ -0,0 +1,24 @@ +package com.app.annotation.model; + +import lombok.*; + +import java.util.HashMap; +import java.util.Map; + +@NoArgsConstructor +@Getter +@Setter +public class TextPositionSelector extends Selector { + private Integer start; + + private Integer end; + + @Override + public Map toJSON() { + Map json = new HashMap<>(); + if (this.type != null) json.put("type", this.type); + if (this.start != null) json.put("start", this.start); + if (this.end != null) json.put("end", this.end); + return json; + } +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/model/TextQuoteSelector.java b/app/annotation/annotation/src/main/java/com/app/annotation/model/TextQuoteSelector.java new file mode 100644 index 00000000..7281d3bd --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/model/TextQuoteSelector.java @@ -0,0 +1,28 @@ +package com.app.annotation.model; + +import lombok.*; + +import java.util.HashMap; +import java.util.Map; + +@NoArgsConstructor +@Getter +@Setter +public class TextQuoteSelector extends Selector { + + private String exact; + + private String prefix; + + private String suffix; + + @Override + public Map toJSON() { + Map json = new HashMap<>(); + if (this.type != null) json.put("type", this.type); + if (this.exact != null) json.put("exact", this.exact); + if (this.prefix != null) json.put("prefix", this.prefix); + if (this.suffix != null) json.put("suffix", this.suffix); + return json; + } +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/repository/AnnotationRepository.java b/app/annotation/annotation/src/main/java/com/app/annotation/repository/AnnotationRepository.java new file mode 100644 index 00000000..7dea72db --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/repository/AnnotationRepository.java @@ -0,0 +1,10 @@ +package com.app.annotation.repository; + +import com.app.annotation.model.Annotation; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; + +public interface AnnotationRepository extends MongoRepository { + List findAllByTargetSource(String source); +} diff --git a/app/annotation/annotation/src/main/java/com/app/annotation/service/AnnotationService.java b/app/annotation/annotation/src/main/java/com/app/annotation/service/AnnotationService.java new file mode 100644 index 00000000..13708d39 --- /dev/null +++ b/app/annotation/annotation/src/main/java/com/app/annotation/service/AnnotationService.java @@ -0,0 +1,138 @@ +package com.app.annotation.service; + +import com.app.annotation.dto.request.SelectorDto; +import com.app.annotation.dto.request.TargetDto; +import com.app.annotation.exception.BadRequestException; +import com.app.annotation.model.*; +import com.app.annotation.repository.AnnotationRepository; +import com.app.annotation.dto.request.CreateAnnotationRequestDto; +import org.modelmapper.ModelMapper; +import org.springframework.stereotype.Service; +import org.modelmapper.PropertyMap; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Service +public class AnnotationService { + + private final AnnotationRepository annotationRepository; + + private final ModelMapper modelMapper; + + + public AnnotationService(AnnotationRepository annotationRepository, ModelMapper modelMapper) { + this.annotationRepository = annotationRepository; + this.modelMapper = modelMapper; + + modelMapper.addMappings(new PropertyMap() { + @Override + protected void configure() { + skip().setSelector(null); + } + }); + } + + public Map createAnnotation(CreateAnnotationRequestDto dto) { + + Optional prevAnnotation = annotationRepository.findById(dto.getId()); + + if (prevAnnotation.isPresent()) { + throw new BadRequestException("There is an annotation with same id."); + } + + Annotation annotationToCreate = modelMapper.map(dto, Annotation.class); + + List selectorList = new ArrayList<>(); + for (SelectorDto s : dto.getTarget().getSelector()) { + switch (s.getType()) { + case "TextQuoteSelector" -> selectorList.add(modelMapper.map(s, TextQuoteSelector.class)); + case "TextPositionSelector" -> selectorList.add(modelMapper.map(s, TextPositionSelector.class)); + case "FragmentSelector" -> selectorList.add(modelMapper.map(s, FragmentSelector.class)); + } + //TODO add more selectors if implemented in the future + } + + annotationToCreate.getTarget().setSelector(selectorList); + + return annotationRepository.save(annotationToCreate).toJSON(); + } + + public boolean deleteAnnotation(String id) { + if(!annotationRepository.existsById(id)) { + return false; + } + annotationRepository.deleteById(id); + return true; + } + + public Map updateAnnotation(CreateAnnotationRequestDto dto) { + Optional annotation = annotationRepository.findById(dto.getId()); + if (annotation.isEmpty()) { + return null; + } + Annotation annotationToUpdate = annotation.get(); + + Annotation updatedAnnotation = modelMapper.map(dto, Annotation.class); + List selectorList = new ArrayList<>(); + for (SelectorDto s : dto.getTarget().getSelector()) { + switch (s.getType()) { + case "TextQuoteSelector" -> selectorList.add(modelMapper.map(s, TextQuoteSelector.class)); + case "TextPositionSelector" -> selectorList.add(modelMapper.map(s, TextPositionSelector.class)); + case "FragmentSelector" -> selectorList.add(modelMapper.map(s, FragmentSelector.class)); + } + //TODO add more selectors if implemented in the future + } + updatedAnnotation.getTarget().setSelector(selectorList); + + annotationToUpdate.setTarget(updatedAnnotation.getTarget()); + annotationToUpdate.setBody(updatedAnnotation.getBody()); + annotationToUpdate.setCreated(updatedAnnotation.getCreated()); + annotationToUpdate.setCreator(updatedAnnotation.getCreator()); + annotationToUpdate.setMotivation(updatedAnnotation.getMotivation()); + + return annotationRepository.save(annotationToUpdate).toJSON(); + } + + public List> getAnnotations(String source) { + List annotations = annotationRepository.findAllByTargetSource(source); + List> jsonAnnotations = new ArrayList<>(); + for (Annotation a : annotations) { + + List selectors = a.getTarget().getSelector(); + + boolean haveTextSelectors = true; + + for (Selector s : selectors) { + if (!s.getType().equals("TextQuoteSelector") && !s.getType().equals("TextPositionSelector")) { + haveTextSelectors = false; + break; + } + + // TODO change logic as more selectors are implemented in the future + } + + if (!haveTextSelectors) { + continue; + } + + jsonAnnotations.add(a.toJSON()); + } + return jsonAnnotations; + } + + public List> getImageAnnotations(String source) { + List annotations = annotationRepository.findAllByTargetSource(source); + List> jsonAnnotations = new ArrayList<>(); + for (Annotation a : annotations) { + + if (a.getTarget().getSelector().size() == 1 && a.getTarget().getSelector().get(0).getType().equals("FragmentSelector")) { + jsonAnnotations.add(a.toJSON()); + } + // TODO change logic if more image selectors are implemented in the future + } + return jsonAnnotations; + } +} diff --git a/app/annotation/annotation/src/main/resources/application.properties b/app/annotation/annotation/src/main/resources/application.properties new file mode 100644 index 00000000..845918c0 --- /dev/null +++ b/app/annotation/annotation/src/main/resources/application.properties @@ -0,0 +1,2 @@ +spring.data.mongodb.database=${MONGO_DATABASE} +spring.data.mongodb.uri=mongodb+srv://${MONGO_USER}:${MONGO_PASSWORD}@${MONGO_CLUSTER} \ No newline at end of file diff --git a/app/backend/.dockerignore b/app/backend/.dockerignore new file mode 100644 index 00000000..9a19c5db --- /dev/null +++ b/app/backend/.dockerignore @@ -0,0 +1,5 @@ +target +images +README.md +format.sh +.gitignore \ No newline at end of file diff --git a/app/backend/.gitignore b/app/backend/.gitignore index ccba6195..4af7fcd1 100644 --- a/app/backend/.gitignore +++ b/app/backend/.gitignore @@ -36,4 +36,5 @@ build/ *.env ### Image Files -/images/ +images/* +!images/README.md diff --git a/app/backend/Dockerfile b/app/backend/Dockerfile index a9bae591..b8df3362 100644 --- a/app/backend/Dockerfile +++ b/app/backend/Dockerfile @@ -1,6 +1,7 @@ -FROM eclipse-temurin:17-jdk-alpine -WORKDIR /app -VOLUME /app/images -COPY target/*.jar /app/bounswe.jar +FROM maven:3.9.6-eclipse-temurin-21-jammy +WORKDIR /app/backend +VOLUME /app/backend/images +COPY . /app/backend/ +RUN mvn clean install EXPOSE 8080 -ENTRYPOINT ["java","-jar","/app/bounswe.jar"] \ No newline at end of file +ENTRYPOINT ["java","-jar","/app/backend/target/gamereview-0.0.1-SNAPSHOT.jar"] \ No newline at end of file diff --git a/app/backend/images/README.md b/app/backend/images/README.md new file mode 100644 index 00000000..56199813 --- /dev/null +++ b/app/backend/images/README.md @@ -0,0 +1 @@ +This directory is where the application stores its images, when it is run locally. \ No newline at end of file diff --git a/app/backend/pom.xml b/app/backend/pom.xml index abd955da..6ff52a17 100644 --- a/app/backend/pom.xml +++ b/app/backend/pom.xml @@ -104,6 +104,25 @@ spring-javaformat-maven-plugin 0.0.39 + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + + prepare-agent + + + + generate-code-coverage-report + test + + report + + + + diff --git a/app/backend/src/main/java/com/app/gamereview/GamereviewApplication.java b/app/backend/src/main/java/com/app/gamereview/GamereviewApplication.java index 88dfc6d5..1501f8ec 100644 --- a/app/backend/src/main/java/com/app/gamereview/GamereviewApplication.java +++ b/app/backend/src/main/java/com/app/gamereview/GamereviewApplication.java @@ -1,7 +1,11 @@ package com.app.gamereview; +import org.apache.coyote.http11.AbstractHttp11Protocol; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.context.annotation.Bean; @SpringBootApplication public class GamereviewApplication { @@ -10,4 +14,11 @@ public static void main(String[] args) { SpringApplication.run(GamereviewApplication.class, args); } + @Bean + public WebServerFactoryCustomizer tomcatCustomizer() { + return (tomcat) -> { + tomcat.addConnectorCustomizers((connector) -> + ((AbstractHttp11Protocol)connector.getProtocolHandler()).setRelaxedQueryChars("[]")); + }; + } } \ No newline at end of file diff --git a/app/backend/src/main/java/com/app/gamereview/config/CorsConfig.java b/app/backend/src/main/java/com/app/gamereview/config/CorsConfig.java index 5c1adb26..cd6f63b0 100644 --- a/app/backend/src/main/java/com/app/gamereview/config/CorsConfig.java +++ b/app/backend/src/main/java/com/app/gamereview/config/CorsConfig.java @@ -15,7 +15,7 @@ public class CorsConfig { @Bean public CorsConfigurationSource corsConfigurationSource() { final CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173", "http://ec2-51-20-78-40.eu-north-1.compute.amazonaws.com/")); + configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173", "http://localhost", "http://ec2-51-20-78-40.eu-north-1.compute.amazonaws.com/")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowCredentials(true); configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "Set-Cookie", "credentials")); diff --git a/app/backend/src/main/java/com/app/gamereview/controller/AchievementController.java b/app/backend/src/main/java/com/app/gamereview/controller/AchievementController.java index b39aef62..185d98d4 100644 --- a/app/backend/src/main/java/com/app/gamereview/controller/AchievementController.java +++ b/app/backend/src/main/java/com/app/gamereview/controller/AchievementController.java @@ -58,7 +58,7 @@ public ResponseEntity deleteAchievement(@RequestParam String id, @R @DeleteMapping("/delete-by-name") @AuthorizationRequired @AdminRequired - public ResponseEntity deleteAchievementByName(@RequestParam String achievementName, @RequestParam String gameName, + public ResponseEntity deleteAchievementByName(@RequestParam String achievementName, @RequestParam(required = false) String gameName, @RequestHeader String Authorization, HttpServletRequest request) { Achievement achievement = achievementService.deleteAchievement(achievementName, gameName); return ResponseEntity.ok(achievement); diff --git a/app/backend/src/main/java/com/app/gamereview/controller/CharacterController.java b/app/backend/src/main/java/com/app/gamereview/controller/CharacterController.java new file mode 100644 index 00000000..0142dbf0 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/controller/CharacterController.java @@ -0,0 +1,61 @@ +package com.app.gamereview.controller; + +import com.app.gamereview.dto.request.character.CreateCharacterRequestDto; +import com.app.gamereview.dto.request.character.UpdateCharacterRequestDto; +import com.app.gamereview.model.Character; +import com.app.gamereview.service.CharacterService; +import com.app.gamereview.util.validation.annotation.AdminRequired; +import com.app.gamereview.util.validation.annotation.AuthorizationRequired; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/character") +@Validated +public class CharacterController { + + private final CharacterService characterService; + + public CharacterController(CharacterService characterService) { + this.characterService = characterService; + } + + @PostMapping("/create") + @AuthorizationRequired + @AdminRequired + public ResponseEntity createCharacter(@Valid @RequestBody CreateCharacterRequestDto characterDto, + @RequestHeader String Authorization, HttpServletRequest request) { + Character character = characterService.createCharacter(characterDto); + return ResponseEntity.ok(character); + } + + @PostMapping("/update") + @AuthorizationRequired + @AdminRequired + public ResponseEntity updateCharacter(@RequestParam String id, + @Valid @RequestBody UpdateCharacterRequestDto updateCharacterRequestDto, + @RequestHeader String Authorization, HttpServletRequest request) { + Character character = characterService.updateCharacter(id, updateCharacterRequestDto); + return ResponseEntity.ok(character); + } + + @DeleteMapping("/delete") + @AuthorizationRequired + @AdminRequired + public ResponseEntity deleteCharacter(@RequestParam String id, @RequestHeader String Authorization, + HttpServletRequest request) { + Character character = characterService.deleteCharacter(id); + return ResponseEntity.ok(character); + } + + @GetMapping("/get-game-characters") + public ResponseEntity> getGameCharacters(@RequestParam String gameId) { + List gameCharacters = characterService.getGameCharacters(gameId); + return ResponseEntity.ok(gameCharacters); + } +} diff --git a/app/backend/src/main/java/com/app/gamereview/controller/GameController.java b/app/backend/src/main/java/com/app/gamereview/controller/GameController.java index 7562152d..014260dd 100644 --- a/app/backend/src/main/java/com/app/gamereview/controller/GameController.java +++ b/app/backend/src/main/java/com/app/gamereview/controller/GameController.java @@ -1,24 +1,26 @@ package com.app.gamereview.controller; -import com.app.gamereview.dto.request.game.CreateGameRequestDto; -import com.app.gamereview.dto.request.game.GetGameListRequestDto; -import com.app.gamereview.dto.request.game.AddGameTagRequestDto; -import com.app.gamereview.dto.request.game.RemoveGameTagRequestDto; +import com.app.gamereview.dto.request.game.*; import com.app.gamereview.dto.response.game.GameDetailResponseDto; import com.app.gamereview.dto.response.game.GetGameListResponseDto; import com.app.gamereview.dto.response.tag.AddGameTagResponseDto; import com.app.gamereview.dto.response.tag.GetAllTagsOfGameResponseDto; import com.app.gamereview.model.Game; +import com.app.gamereview.model.User; import com.app.gamereview.service.GameService; +import com.app.gamereview.util.JwtUtil; import com.app.gamereview.util.validation.annotation.AdminRequired; import com.app.gamereview.util.validation.annotation.AuthorizationRequired; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import org.springdoc.core.annotations.ParameterObject; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.HashSet; import java.util.List; @RestController @@ -41,8 +43,8 @@ public ResponseEntity> getAllGames(@RequestBody(req } @GetMapping("get-game-list") - public ResponseEntity> getGames(@ParameterObject GetGameListRequestDto filter) { - List gameList = gameService.getGames(filter); + public ResponseEntity> getGames(@ParameterObject GetGameListRequestDto filter) { + List gameList = gameService.getGames(filter); return ResponseEntity.ok(gameList); } @@ -88,4 +90,44 @@ public ResponseEntity createGame(@Valid @RequestBody CreateGameRequestDto Game gameToCreate = gameService.createGame(createGameRequestDto); return ResponseEntity.ok(gameToCreate); } + + @AuthorizationRequired + @PutMapping("/update") + public ResponseEntity editGame(@RequestParam String id, + @Valid @RequestBody UpdateGameRequestDto updateGameRequestDto, + @RequestHeader String Authorization, HttpServletRequest request) { + Game updatedGame = gameService.editGame(id, updateGameRequestDto); + return ResponseEntity.ok(updatedGame); + } + + @AuthorizationRequired + @AdminRequired + @DeleteMapping("/delete") + public ResponseEntity deleteGame(@RequestParam String id, @RequestHeader String Authorization, + HttpServletRequest request) { + Boolean isDeleted = gameService.deleteGame(id); + return ResponseEntity.ok(isDeleted); + } + + @GetMapping("/get-recommendations") + public ResponseEntity> getRecommendedGames(@RequestHeader(name = HttpHeaders.AUTHORIZATION, + required = false) String Authorization) { + if(Authorization == null){ + return ResponseEntity.ok(gameService.getRecommendedGames()); + } + String email = null; + if (JwtUtil.validateToken(Authorization)) + email = JwtUtil.extractSubject(Authorization); + List games = gameService.getRecommendedGames(email); + return ResponseEntity.ok(games); + } + + @AuthorizationRequired + @AdminRequired + @PostMapping("/promote") + public ResponseEntity changePromotionStatus(@RequestParam String id,@RequestHeader String Authorization, + HttpServletRequest request){ + Game gameToPromote = gameService.changePromotionStatusOfGame(id); + return ResponseEntity.ok(gameToPromote); + } } diff --git a/app/backend/src/main/java/com/app/gamereview/controller/GroupController.java b/app/backend/src/main/java/com/app/gamereview/controller/GroupController.java index 24f5746c..f2ee86e2 100644 --- a/app/backend/src/main/java/com/app/gamereview/controller/GroupController.java +++ b/app/backend/src/main/java/com/app/gamereview/controller/GroupController.java @@ -3,7 +3,9 @@ import com.app.gamereview.dto.request.group.*; import com.app.gamereview.dto.response.group.GetGroupDetailResponseDto; import com.app.gamereview.dto.response.group.GetGroupResponseDto; +import com.app.gamereview.dto.response.group.GroupApplicationResponseDto; import com.app.gamereview.dto.response.tag.AddGroupTagResponseDto; +import com.app.gamereview.model.Game; import com.app.gamereview.model.Group; import com.app.gamereview.model.User; import com.app.gamereview.service.GroupService; @@ -20,135 +22,179 @@ import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Optional; @RestController @RequestMapping("/api/group") @Validated public class GroupController { - private final GroupService groupService; + private final GroupService groupService; - @Autowired - public GroupController( - GroupService groupService - ) { - this.groupService = groupService; - } + @Autowired + public GroupController( + GroupService groupService + ) { + this.groupService = groupService; + } - @GetMapping("/get-all") - public ResponseEntity> getGroups( - @ParameterObject GetAllGroupsFilterRequestDto filter, @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String Authorization) { + @GetMapping("/get-all") + public ResponseEntity> getGroups( + @ParameterObject GetAllGroupsFilterRequestDto filter, @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String Authorization) { - String email; + String email; if (JwtUtil.validateToken(Authorization)) email = JwtUtil.extractSubject(Authorization); else email = ""; List groups = groupService.getAllGroups(filter, email); - return ResponseEntity.ok(groups); - } - - @GetMapping("/get") - public ResponseEntity getGroup( - @RequestParam String id, @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String Authorization) { - - String email; - if (JwtUtil.validateToken(Authorization)) email = JwtUtil.extractSubject(Authorization); - else email = ""; - - GetGroupDetailResponseDto group = groupService.getGroupById(id, email); - - return ResponseEntity.ok(group); - } - - @AuthorizationRequired - @PostMapping("/create") - public ResponseEntity createGroup(@Valid @RequestBody CreateGroupRequestDto createGroupRequestDto, - @RequestHeader String Authorization, HttpServletRequest request) { - User user = (User) request.getAttribute("authenticatedUser"); - Group groupToCreate = groupService.createGroup(createGroupRequestDto, user); - return ResponseEntity.ok(groupToCreate); - } - - @AuthorizationRequired - @PutMapping("/update") - public ResponseEntity editGroup(@RequestParam String id, - @Valid @RequestBody UpdateGroupRequestDto updateGroupRequestDto, - @RequestHeader String Authorization, HttpServletRequest request) { - Group updatedGroup = groupService.updateGroup(id,updateGroupRequestDto); - return ResponseEntity.ok(updatedGroup); - } - - @AuthorizationRequired - @DeleteMapping("/delete") - public ResponseEntity deleteGroup(String identifier, - @RequestHeader String Authorization, HttpServletRequest request) { - User user = (User) request.getAttribute("authenticatedUser"); - Boolean response = groupService.deleteGroup(identifier); - return ResponseEntity.ok(response); - } - - @AuthorizationRequired - @PostMapping("/add-tag") - public ResponseEntity addGroupTag( - @Valid @RequestBody AddGroupTagRequestDto addGroupTagRequestDto) { - AddGroupTagResponseDto response = groupService.addGroupTag(addGroupTagRequestDto); - return ResponseEntity.ok(response); - } - - @AuthorizationRequired - @DeleteMapping("/remove-tag") - public ResponseEntity removeGroupTag( - @Valid @RequestBody RemoveGroupTagRequestDto removeGroupTagRequestDto) { - Boolean response = groupService.removeGroupTag(removeGroupTagRequestDto); - return ResponseEntity.ok(response); - } - - @AuthorizationRequired - @PostMapping("/join") - public ResponseEntity joinGroup(@RequestParam String id, - @RequestHeader String Authorization, HttpServletRequest request) { - User user = (User) request.getAttribute("authenticatedUser"); - Boolean joined = groupService.joinGroup(id, user); - return ResponseEntity.ok(joined); - } - - @AuthorizationRequired - @PostMapping("/leave") - public ResponseEntity leaveGroup(@RequestParam String id, - @RequestHeader String Authorization, HttpServletRequest request) { - User user = (User) request.getAttribute("authenticatedUser"); - Boolean leave = groupService.leaveGroup(id, user); - return ResponseEntity.ok(leave); - } - - @AuthorizationRequired - @PutMapping("/ban-user") - public ResponseEntity banUser(@RequestParam String groupId, @RequestParam String userId, @RequestHeader String Authorization, HttpServletRequest request) { - User user = (User) request.getAttribute("authenticatedUser"); - Boolean result = groupService.banUser(groupId, userId, user); - return ResponseEntity.ok(result); - } - @AuthorizationRequired - @PutMapping("/add-moderator") - public ResponseEntity addModerator(@RequestParam String groupId, @RequestParam String userId, @RequestHeader String Authorization, HttpServletRequest request) { - User user = (User) request.getAttribute("authenticatedUser"); - Boolean result = groupService.addModerator(groupId, userId, user); - return ResponseEntity.ok(result); - } - - @AuthorizationRequired - @AdminRequired - @PutMapping("/remove-moderator") - public ResponseEntity removeModerator(@RequestParam String groupId, @RequestParam String userId, @RequestHeader String Authorization, HttpServletRequest request) { - Boolean result = groupService.removeModerator(groupId, userId); - return ResponseEntity.ok(result); - } - - @AuthorizationRequired - @PutMapping("/unban-user") - public ResponseEntity unbanUser(@RequestParam String groupId, @RequestParam String userId, @RequestHeader String Authorization, HttpServletRequest request) { - User user = (User) request.getAttribute("authenticatedUser"); - Boolean result = groupService.unbanUser(groupId, userId, user); - return ResponseEntity.ok(result); - } + return ResponseEntity.ok(groups); + } + + @GetMapping("/get") + public ResponseEntity getGroup( + @RequestParam String id, @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String Authorization) { + + String email; + if (JwtUtil.validateToken(Authorization)) email = JwtUtil.extractSubject(Authorization); + else email = ""; + + GetGroupDetailResponseDto group = groupService.getGroupById(id, email); + + return ResponseEntity.ok(group); + } + + @AuthorizationRequired + @PostMapping("/create") + public ResponseEntity createGroup(@Valid @RequestBody CreateGroupRequestDto createGroupRequestDto, + @RequestHeader String Authorization, HttpServletRequest request) { + User user = (User) request.getAttribute("authenticatedUser"); + Group groupToCreate = groupService.createGroup(createGroupRequestDto, user); + return ResponseEntity.ok(groupToCreate); + } + + @AuthorizationRequired + @PutMapping("/update") + public ResponseEntity editGroup(@RequestParam String id, + @Valid @RequestBody UpdateGroupRequestDto updateGroupRequestDto, + @RequestHeader String Authorization, HttpServletRequest request) { + Group updatedGroup = groupService.updateGroup(id, updateGroupRequestDto); + return ResponseEntity.ok(updatedGroup); + } + + @AuthorizationRequired + @DeleteMapping("/delete") + public ResponseEntity deleteGroup(String identifier, + @RequestHeader String Authorization, HttpServletRequest request) { + User user = (User) request.getAttribute("authenticatedUser"); + Boolean response = groupService.deleteGroup(identifier); + return ResponseEntity.ok(response); + } + + @AuthorizationRequired + @PostMapping("/add-tag") + public ResponseEntity addGroupTag( + @Valid @RequestBody AddGroupTagRequestDto addGroupTagRequestDto) { + AddGroupTagResponseDto response = groupService.addGroupTag(addGroupTagRequestDto); + return ResponseEntity.ok(response); + } + + @AuthorizationRequired + @DeleteMapping("/remove-tag") + public ResponseEntity removeGroupTag( + @Valid @RequestBody RemoveGroupTagRequestDto removeGroupTagRequestDto) { + Boolean response = groupService.removeGroupTag(removeGroupTagRequestDto); + return ResponseEntity.ok(response); + } + + @AuthorizationRequired + @PostMapping("/join") + public ResponseEntity joinGroup(@RequestParam String id, + @RequestHeader String Authorization, HttpServletRequest request) { + User user = (User) request.getAttribute("authenticatedUser"); + Boolean joined = groupService.joinGroup(id, user); + return ResponseEntity.ok(joined); + } + + @AuthorizationRequired + @PostMapping("/leave") + public ResponseEntity leaveGroup(@RequestParam String id, + @RequestHeader String Authorization, HttpServletRequest request) { + User user = (User) request.getAttribute("authenticatedUser"); + Boolean leave = groupService.leaveGroup(id, user); + return ResponseEntity.ok(leave); + } + + @AuthorizationRequired + @PutMapping("/ban-user") + public ResponseEntity banUser(@RequestParam String groupId, @RequestParam String userId, @RequestHeader String Authorization, HttpServletRequest request) { + User user = (User) request.getAttribute("authenticatedUser"); + Boolean result = groupService.banUser(groupId, userId, user); + return ResponseEntity.ok(result); + } + + @AuthorizationRequired + @PutMapping("/add-moderator") + public ResponseEntity addModerator(@RequestParam String groupId, @RequestParam String userId, @RequestHeader String Authorization, HttpServletRequest request) { + User user = (User) request.getAttribute("authenticatedUser"); + Boolean result = groupService.addModerator(groupId, userId, user); + return ResponseEntity.ok(result); + } + + @AuthorizationRequired + @AdminRequired + @PutMapping("/remove-moderator") + public ResponseEntity removeModerator(@RequestParam String groupId, @RequestParam String userId, @RequestHeader String Authorization, HttpServletRequest request) { + Boolean result = groupService.removeModerator(groupId, userId); + return ResponseEntity.ok(result); + } + + @AuthorizationRequired + @PutMapping("/unban-user") + public ResponseEntity unbanUser(@RequestParam String groupId, @RequestParam String userId, @RequestHeader String Authorization, HttpServletRequest request) { + User user = (User) request.getAttribute("authenticatedUser"); + Boolean result = groupService.unbanUser(groupId, userId, user); + return ResponseEntity.ok(result); + } + + @AuthorizationRequired + @PostMapping("/apply") + public ResponseEntity applyGroup(@RequestParam String groupId, + @RequestBody GroupApplicationRequestDto dto, + @RequestHeader String Authorization, HttpServletRequest request) { + User user = (User) request.getAttribute("authenticatedUser"); + Boolean applied = groupService.applyGroup(groupId, user, dto); + return ResponseEntity.ok(applied); + } + + @AuthorizationRequired + @PostMapping("/review-application") + public ResponseEntity reviewApplication(@RequestParam String applicationId, + @Valid @RequestBody GroupApplicationReviewDto dto, + @RequestHeader String Authorization, HttpServletRequest request) { + User user = (User) request.getAttribute("authenticatedUser"); + Boolean reviewed = groupService.reviewApplication(applicationId, user, dto); + return ResponseEntity.ok(reviewed); + } + + @AuthorizationRequired + @GetMapping("/list-applications") + public ResponseEntity> listApplications(@RequestParam String groupId, + @RequestHeader String Authorization, HttpServletRequest request) { + User user = (User) request.getAttribute("authenticatedUser"); + List applications = groupService.listApplications(groupId, user); + return ResponseEntity.ok(applications); + } + + @GetMapping("/get-recommendations") + public ResponseEntity> getRecommendedGroups(@RequestHeader(name = HttpHeaders.AUTHORIZATION, + required = false) String Authorization) { + String email = ""; + if(Authorization == null){ + return ResponseEntity.ok(groupService.getRecommendedGroups(email)); + } + if (JwtUtil.validateToken(Authorization)) + email = JwtUtil.extractSubject(Authorization); + List groups = groupService.getRecommendedGroups(email); + return ResponseEntity.ok(groups); + } } diff --git a/app/backend/src/main/java/com/app/gamereview/controller/HomeController.java b/app/backend/src/main/java/com/app/gamereview/controller/HomeController.java new file mode 100644 index 00000000..f150c7e6 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/controller/HomeController.java @@ -0,0 +1,43 @@ +package com.app.gamereview.controller; + +import com.app.gamereview.dto.request.home.HomePagePostsFilterRequestDto; +import com.app.gamereview.dto.response.home.HomePagePostResponseDto; +import com.app.gamereview.model.Post; +import com.app.gamereview.model.User; +import com.app.gamereview.service.PostService; +import com.app.gamereview.util.JwtUtil; +import com.app.gamereview.util.validation.annotation.AuthorizationRequired; +import jakarta.servlet.http.HttpServletRequest; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/home") +@Validated +public class HomeController { + + private final PostService postService; + + @Autowired + public HomeController(PostService postService) { + this.postService = postService; + } + + @GetMapping + public ResponseEntity> getHomePagePosts(@ParameterObject HomePagePostsFilterRequestDto filter, + @RequestHeader(name = HttpHeaders.AUTHORIZATION, + required = false) String Authorization){ + String email = null; + if (JwtUtil.validateToken(Authorization)) + email = JwtUtil.extractSubject(Authorization); + List postsToShow = postService.getHomepagePosts(filter, email); + return ResponseEntity.ok(postsToShow); + } + +} diff --git a/app/backend/src/main/java/com/app/gamereview/controller/NotificationController.java b/app/backend/src/main/java/com/app/gamereview/controller/NotificationController.java new file mode 100644 index 00000000..f4fe93ad --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/controller/NotificationController.java @@ -0,0 +1,32 @@ +package com.app.gamereview.controller; + +import com.app.gamereview.dto.request.notification.GetNotificationsRequestDto; +import com.app.gamereview.model.Notification; +import com.app.gamereview.model.User; +import com.app.gamereview.service.NotificationService; +import com.app.gamereview.util.validation.annotation.AuthorizationRequired; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/notification") +public class NotificationController { + private final NotificationService notificationService; + + @Autowired + public NotificationController(NotificationService notificationService) { + this.notificationService = notificationService; + } + + @AuthorizationRequired + @GetMapping("/get-notifications") + public ResponseEntity> getNotificationList(@RequestHeader String Authorization, @RequestBody(required = false) GetNotificationsRequestDto filter, HttpServletRequest request) { + User user = (User) request.getAttribute("authenticatedUser"); + List notifications = notificationService.getNotificationList(user.getId(), filter); + return ResponseEntity.ok(notifications); + } +} diff --git a/app/backend/src/main/java/com/app/gamereview/controller/PostController.java b/app/backend/src/main/java/com/app/gamereview/controller/PostController.java index cae7cbcb..522bfad1 100644 --- a/app/backend/src/main/java/com/app/gamereview/controller/PostController.java +++ b/app/backend/src/main/java/com/app/gamereview/controller/PostController.java @@ -57,11 +57,12 @@ public ResponseEntity getPostDetail(@RequestParam Stri return ResponseEntity.ok(post); } - @AuthorizationRequired @GetMapping("/get-post-comments") - public ResponseEntity> getPostComments(@RequestParam String id, @RequestHeader String Authorization, HttpServletRequest request) { - User user = (User) request.getAttribute("authenticatedUser"); - List comments = postService.getCommentList(id, user); + public ResponseEntity> getPostComments(@RequestParam String id, @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String Authorization) { + String email; + if (JwtUtil.validateToken(Authorization)) email = JwtUtil.extractSubject(Authorization); + else email = ""; + List comments = postService.getCommentList(id, email); return ResponseEntity.ok(comments); } diff --git a/app/backend/src/main/java/com/app/gamereview/dto/request/character/CreateCharacterRequestDto.java b/app/backend/src/main/java/com/app/gamereview/dto/request/character/CreateCharacterRequestDto.java new file mode 100644 index 00000000..6ad7a7f6 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/dto/request/character/CreateCharacterRequestDto.java @@ -0,0 +1,49 @@ +package com.app.gamereview.dto.request.character; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CreateCharacterRequestDto { + + @NotEmpty(message = "Character name cannot be empty.") + private String name; + + @NotEmpty(message = "Character icon cannot be empty.") + private String icon; + + @NotEmpty(message = "Character description cannot be empty.") + private String description; + + @NotEmpty(message = "Game list cannot be empty.") + private List<@Pattern(regexp = "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", + message = "Game has invalid Id (UUID) format")String> games; + + private String type; + + private String gender; + + private String race; + + private String status; + + private String occupation; + + private String birthDate; + + private String voiceActor; + + private String height; + + private String age; + + private Map customFields; +} diff --git a/app/backend/src/main/java/com/app/gamereview/dto/request/character/UpdateCharacterRequestDto.java b/app/backend/src/main/java/com/app/gamereview/dto/request/character/UpdateCharacterRequestDto.java new file mode 100644 index 00000000..b9c2a9e1 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/dto/request/character/UpdateCharacterRequestDto.java @@ -0,0 +1,44 @@ +package com.app.gamereview.dto.request.character; + +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class UpdateCharacterRequestDto { + + private String name; + + private String icon; + + private String description; + + private List<@Pattern(regexp = "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", + message = "Game has invalid Id (UUID) format")String> games; + + private String type; + + private String gender; + + private String race; + + private String status; + + private String occupation; + + private String birthDate; + + private String voiceActor; + + private String height; + + private String age; + + private Map customFields; +} diff --git a/app/backend/src/main/java/com/app/gamereview/dto/request/game/RecommendGameDto.java b/app/backend/src/main/java/com/app/gamereview/dto/request/game/RecommendGameDto.java new file mode 100644 index 00000000..980c78ae --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/dto/request/game/RecommendGameDto.java @@ -0,0 +1,34 @@ +package com.app.gamereview.dto.request.game; + +import com.app.gamereview.model.Game; +import lombok.Getter; +import lombok.Setter; + +import java.util.Objects; + +@Getter +@Setter +public class RecommendGameDto implements Comparable{ + private Game game; + private int score = Integer.MAX_VALUE; + + @Override + public int compareTo(RecommendGameDto o) { + if(game.getGameName().equals(o.getGame().getGameName())) return 0; + if(score == o.score) return 1; + return Integer.compare(this.score, o.score); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RecommendGameDto given = (RecommendGameDto) o; + return this.game.getGameName().equals(given.getGame().getGameName()); + } + + @Override + public int hashCode() { + return Objects.hash(game.getGameName()); + } +} diff --git a/app/backend/src/main/java/com/app/gamereview/dto/request/game/UpdateGameRequestDto.java b/app/backend/src/main/java/com/app/gamereview/dto/request/game/UpdateGameRequestDto.java new file mode 100644 index 00000000..2f350740 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/dto/request/game/UpdateGameRequestDto.java @@ -0,0 +1,27 @@ +package com.app.gamereview.dto.request.game; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +@Getter +@Setter +public class UpdateGameRequestDto { + @NotEmpty(message = "Game name cannot be null or empty") + private String gameName; + + @NotEmpty(message = "Game description cannot be null or empty") + private String gameDescription; + + @NotEmpty(message = "Game icon cannot be null or empty") + private String gameIcon; + + @NotNull(message = "Release Date cannot be null or empty") + private Date releaseDate; + + @NotEmpty(message = "Minimum system requirements cannot be null or empty") + private String minSystemReq; +} diff --git a/app/backend/src/main/java/com/app/gamereview/dto/request/group/CreateGroupRequestDto.java b/app/backend/src/main/java/com/app/gamereview/dto/request/group/CreateGroupRequestDto.java index 823fb515..a2f7bcf7 100644 --- a/app/backend/src/main/java/com/app/gamereview/dto/request/group/CreateGroupRequestDto.java +++ b/app/backend/src/main/java/com/app/gamereview/dto/request/group/CreateGroupRequestDto.java @@ -32,6 +32,8 @@ public class CreateGroupRequestDto { message = "Game has Invalid Id (UUID) format") private String gameId; // id of related game + private String groupIcon; + @Positive(message = "Quota cannot be negative or zero") private int quota; diff --git a/app/backend/src/main/java/com/app/gamereview/dto/request/group/GroupApplicationRequestDto.java b/app/backend/src/main/java/com/app/gamereview/dto/request/group/GroupApplicationRequestDto.java new file mode 100644 index 00000000..52f0f5c4 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/dto/request/group/GroupApplicationRequestDto.java @@ -0,0 +1,16 @@ +package com.app.gamereview.dto.request.group; + +import jakarta.validation.constraints.*; +import lombok.Getter; +import lombok.Setter; + + +@Getter +@Setter +public class GroupApplicationRequestDto { + + @NotEmpty(message = "Application message cannot be null or empty") + @Size(min = 10, message = "Application message must be at least 10 characters long") + @Size(max = 300, message = "Application message must be at most 300 characters long") + private String message; +} diff --git a/app/backend/src/main/java/com/app/gamereview/dto/request/group/GroupApplicationReviewDto.java b/app/backend/src/main/java/com/app/gamereview/dto/request/group/GroupApplicationReviewDto.java new file mode 100644 index 00000000..470d4980 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/dto/request/group/GroupApplicationReviewDto.java @@ -0,0 +1,17 @@ +package com.app.gamereview.dto.request.group; + +import com.app.gamereview.enums.GroupApplicationReviewResult; +import com.app.gamereview.util.validation.annotation.ValidApplicationReviewResult; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; + + +@Getter +@Setter +public class GroupApplicationReviewDto { + + @NotEmpty(message = "Application message cannot be null or empty") + @ValidApplicationReviewResult(allowedValues = {GroupApplicationReviewResult.APPROVE, GroupApplicationReviewResult.REJECT}) + private String result; +} diff --git a/app/backend/src/main/java/com/app/gamereview/dto/request/group/RecommendGroupDto.java b/app/backend/src/main/java/com/app/gamereview/dto/request/group/RecommendGroupDto.java new file mode 100644 index 00000000..025fc58f --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/dto/request/group/RecommendGroupDto.java @@ -0,0 +1,34 @@ +package com.app.gamereview.dto.request.group; + +import com.app.gamereview.model.Group; +import lombok.Getter; +import lombok.Setter; + +import java.util.Objects; + +@Getter +@Setter +public class RecommendGroupDto implements Comparable{ + private Group group; + private int score = Integer.MAX_VALUE; + + @Override + public int compareTo(RecommendGroupDto o) { + if(group.getId().equals(o.getGroup().getId())) return 0; + if(score == o.score) return 1; + return Integer.compare(this.score, o.score); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RecommendGroupDto given = (RecommendGroupDto) o; + return this.group.getId().equals(given.getGroup().getId()); + } + + @Override + public int hashCode() { + return Objects.hash(group.getId()); + } +} diff --git a/app/backend/src/main/java/com/app/gamereview/dto/request/group/UpdateGroupRequestDto.java b/app/backend/src/main/java/com/app/gamereview/dto/request/group/UpdateGroupRequestDto.java index 6f02b66c..70ebb90a 100644 --- a/app/backend/src/main/java/com/app/gamereview/dto/request/group/UpdateGroupRequestDto.java +++ b/app/backend/src/main/java/com/app/gamereview/dto/request/group/UpdateGroupRequestDto.java @@ -25,6 +25,8 @@ public class UpdateGroupRequestDto { @ValidMemberPolicy(allowedValues = {MembershipPolicy.PUBLIC, MembershipPolicy.PRIVATE}) private String membershipPolicy; + private String groupIcon; + @Positive(message = "Quota cannot be negative or zero") private int quota; diff --git a/app/backend/src/main/java/com/app/gamereview/dto/request/home/HomePagePostsFilterRequestDto.java b/app/backend/src/main/java/com/app/gamereview/dto/request/home/HomePagePostsFilterRequestDto.java new file mode 100644 index 00000000..4b1aab58 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/dto/request/home/HomePagePostsFilterRequestDto.java @@ -0,0 +1,18 @@ +package com.app.gamereview.dto.request.home; + +import com.app.gamereview.enums.SortDirection; +import com.app.gamereview.enums.SortType; +import com.app.gamereview.util.validation.annotation.ValidSortDirection; +import com.app.gamereview.util.validation.annotation.ValidSortType; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class HomePagePostsFilterRequestDto { + @ValidSortType(allowedValues = {SortType.OVERALL_VOTE, SortType.CREATION_DATE, SortType.VOTE_COUNT}) + private String sortBy = SortType.CREATION_DATE.name(); + + @ValidSortDirection(allowedValues = {SortDirection.ASCENDING, SortDirection.DESCENDING}) + private String sortDirection = SortDirection.DESCENDING.name(); +} diff --git a/app/backend/src/main/java/com/app/gamereview/dto/request/notification/CreateNotificationRequestDto.java b/app/backend/src/main/java/com/app/gamereview/dto/request/notification/CreateNotificationRequestDto.java new file mode 100644 index 00000000..9d94e380 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/dto/request/notification/CreateNotificationRequestDto.java @@ -0,0 +1,18 @@ +package com.app.gamereview.dto.request.notification; + +import com.app.gamereview.enums.NotificationParent; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class CreateNotificationRequestDto { + private String parent; + private NotificationParent parentType; + private String message; + private String user; +} diff --git a/app/backend/src/main/java/com/app/gamereview/dto/request/notification/GetNotificationsRequestDto.java b/app/backend/src/main/java/com/app/gamereview/dto/request/notification/GetNotificationsRequestDto.java new file mode 100644 index 00000000..02aefbc1 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/dto/request/notification/GetNotificationsRequestDto.java @@ -0,0 +1,10 @@ +package com.app.gamereview.dto.request.notification; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class GetNotificationsRequestDto { + private Boolean isRead; +} diff --git a/app/backend/src/main/java/com/app/gamereview/dto/request/post/CreatePostRequestDto.java b/app/backend/src/main/java/com/app/gamereview/dto/request/post/CreatePostRequestDto.java index a6903b5d..39fb660d 100644 --- a/app/backend/src/main/java/com/app/gamereview/dto/request/post/CreatePostRequestDto.java +++ b/app/backend/src/main/java/com/app/gamereview/dto/request/post/CreatePostRequestDto.java @@ -37,4 +37,8 @@ public class CreatePostRequestDto { @Pattern(regexp = "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", message = "Achievement has invalid Id (UUID) format") private String achievement; + + @Pattern(regexp = "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", + message = "Character has invalid Id (UUID) format") + private String character; } diff --git a/app/backend/src/main/java/com/app/gamereview/dto/response/comment/CommentReplyResponseDto.java b/app/backend/src/main/java/com/app/gamereview/dto/response/comment/CommentReplyResponseDto.java index 4764a7fb..145f64ba 100644 --- a/app/backend/src/main/java/com/app/gamereview/dto/response/comment/CommentReplyResponseDto.java +++ b/app/backend/src/main/java/com/app/gamereview/dto/response/comment/CommentReplyResponseDto.java @@ -1,5 +1,6 @@ package com.app.gamereview.dto.response.comment; +import com.app.gamereview.enums.VoteChoice; import com.app.gamereview.model.User; import lombok.AllArgsConstructor; import lombok.Data; @@ -32,6 +33,8 @@ public class CommentReplyResponseDto { private int voteCount; // voteCount = # of upvote + # of downvote + private VoteChoice userVote; + // TODO reports // TODO annotations } diff --git a/app/backend/src/main/java/com/app/gamereview/dto/response/comment/GetPostCommentsResponseDto.java b/app/backend/src/main/java/com/app/gamereview/dto/response/comment/GetPostCommentsResponseDto.java index 71468bed..5541eaad 100644 --- a/app/backend/src/main/java/com/app/gamereview/dto/response/comment/GetPostCommentsResponseDto.java +++ b/app/backend/src/main/java/com/app/gamereview/dto/response/comment/GetPostCommentsResponseDto.java @@ -1,5 +1,6 @@ package com.app.gamereview.dto.response.comment; +import com.app.gamereview.enums.VoteChoice; import com.app.gamereview.model.User; import lombok.AllArgsConstructor; import lombok.Data; @@ -35,6 +36,8 @@ public class GetPostCommentsResponseDto { private ArrayList replies; + private VoteChoice userVote; + // TODO reports // TODO annotations } diff --git a/app/backend/src/main/java/com/app/gamereview/dto/response/group/GetGroupDetailResponseDto.java b/app/backend/src/main/java/com/app/gamereview/dto/response/group/GetGroupDetailResponseDto.java index f7c20b85..6179da54 100644 --- a/app/backend/src/main/java/com/app/gamereview/dto/response/group/GetGroupDetailResponseDto.java +++ b/app/backend/src/main/java/com/app/gamereview/dto/response/group/GetGroupDetailResponseDto.java @@ -27,6 +27,8 @@ public class GetGroupDetailResponseDto { private String forumId; // id of the forum of the group + private String groupIcon; + private int quota; private List moderators = new ArrayList<>(); // userIds of the moderators diff --git a/app/backend/src/main/java/com/app/gamereview/dto/response/group/GetGroupResponseDto.java b/app/backend/src/main/java/com/app/gamereview/dto/response/group/GetGroupResponseDto.java index b1234c61..d987c9aa 100644 --- a/app/backend/src/main/java/com/app/gamereview/dto/response/group/GetGroupResponseDto.java +++ b/app/backend/src/main/java/com/app/gamereview/dto/response/group/GetGroupResponseDto.java @@ -27,6 +27,8 @@ public class GetGroupResponseDto { private String forumId; // id of the forum of the group + private String groupIcon; + private int quota; private List moderators = new ArrayList<>(); // userIds of the moderators diff --git a/app/backend/src/main/java/com/app/gamereview/dto/response/group/GroupApplicationResponseDto.java b/app/backend/src/main/java/com/app/gamereview/dto/response/group/GroupApplicationResponseDto.java new file mode 100644 index 00000000..362ed3eb --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/dto/response/group/GroupApplicationResponseDto.java @@ -0,0 +1,25 @@ +package com.app.gamereview.dto.response.group; + +import com.app.gamereview.enums.GroupApplicationStatus; +import com.app.gamereview.model.Group; +import com.app.gamereview.model.User; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +public class GroupApplicationResponseDto { + private String id; + + private LocalDateTime appliedAt; + + private User applicant; + + private Group group; + + private String message; + + private GroupApplicationStatus status; +} diff --git a/app/backend/src/main/java/com/app/gamereview/dto/response/home/HomePagePostResponseDto.java b/app/backend/src/main/java/com/app/gamereview/dto/response/home/HomePagePostResponseDto.java new file mode 100644 index 00000000..761655f8 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/dto/response/home/HomePagePostResponseDto.java @@ -0,0 +1,65 @@ +package com.app.gamereview.dto.response.home; + +import com.app.gamereview.enums.ForumType; +import com.app.gamereview.enums.VoteChoice; +import com.app.gamereview.model.Achievement; +import com.app.gamereview.model.Character; +import com.app.gamereview.model.Tag; +import com.app.gamereview.model.User; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +public class HomePagePostResponseDto { + + private String id; + + private String title; + + private String postContent; + + private String postImage; + + private User poster; + + private Achievement achievement; + + private Character character; + + private VoteChoice userVote; + + @NotNull + private String forum; + + private ForumType type; + + private String typeId; + + private String typeName; + + // TODO avatar + + private LocalDateTime lastEditedAt; + + private List tags = new ArrayList<>(); + + private Boolean inappropriate; + + private Boolean locked; + + private int overallVote; // overallVote = # of upvote - # of downvote + + private int voteCount; // voteCount = # of upvote + # of downvote + + private LocalDateTime createdAt; + + private Boolean isDeleted; + private Boolean isPromoted; + +} diff --git a/app/backend/src/main/java/com/app/gamereview/dto/response/post/GetPostDetailResponseDto.java b/app/backend/src/main/java/com/app/gamereview/dto/response/post/GetPostDetailResponseDto.java index 95895686..caf425af 100644 --- a/app/backend/src/main/java/com/app/gamereview/dto/response/post/GetPostDetailResponseDto.java +++ b/app/backend/src/main/java/com/app/gamereview/dto/response/post/GetPostDetailResponseDto.java @@ -4,6 +4,7 @@ import com.app.gamereview.enums.VoteChoice; import com.app.gamereview.model.Tag; import com.app.gamereview.model.User; +import com.app.gamereview.model.Character; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -32,6 +33,8 @@ public class GetPostDetailResponseDto { private Achievement achievement; + private Character character; + private LocalDateTime lastEditedAt; private List tags; diff --git a/app/backend/src/main/java/com/app/gamereview/dto/response/post/GetPostListResponseDto.java b/app/backend/src/main/java/com/app/gamereview/dto/response/post/GetPostListResponseDto.java index 2b507c27..536615e0 100644 --- a/app/backend/src/main/java/com/app/gamereview/dto/response/post/GetPostListResponseDto.java +++ b/app/backend/src/main/java/com/app/gamereview/dto/response/post/GetPostListResponseDto.java @@ -7,6 +7,7 @@ import com.app.gamereview.enums.VoteChoice; import com.app.gamereview.model.Tag; import com.app.gamereview.model.User; +import com.app.gamereview.model.Character; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -30,6 +31,8 @@ public class GetPostListResponseDto { private Achievement achievement; + private Character character; + // TODO avatar private LocalDateTime lastEditedAt; diff --git a/app/backend/src/main/java/com/app/gamereview/enums/GroupApplicationReviewResult.java b/app/backend/src/main/java/com/app/gamereview/enums/GroupApplicationReviewResult.java new file mode 100644 index 00000000..e44a9a43 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/enums/GroupApplicationReviewResult.java @@ -0,0 +1,6 @@ +package com.app.gamereview.enums; + +public enum GroupApplicationReviewResult { + APPROVE, + REJECT +} diff --git a/app/backend/src/main/java/com/app/gamereview/enums/GroupApplicationStatus.java b/app/backend/src/main/java/com/app/gamereview/enums/GroupApplicationStatus.java new file mode 100644 index 00000000..282a9dfa --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/enums/GroupApplicationStatus.java @@ -0,0 +1,7 @@ +package com.app.gamereview.enums; + +public enum GroupApplicationStatus { + PENDING, + APPROVED, + REJECTED +} diff --git a/app/backend/src/main/java/com/app/gamereview/enums/NotificationMessage.java b/app/backend/src/main/java/com/app/gamereview/enums/NotificationMessage.java new file mode 100644 index 00000000..63477909 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/enums/NotificationMessage.java @@ -0,0 +1,24 @@ +package com.app.gamereview.enums; + +import lombok.Getter; + + @Getter + public enum NotificationMessage { + FIRST_POST_ACHIEVEMENT("Way to go, {user_name}! 🎉 You've unlocked the 'First Post' achievement by starting a discussion in the {forum_name} forum. Your contributions make our community vibrant!"), + FIRST_REVIEW_ACHIEVEMENT("Awesome work, {user_name}! 🏆 You've achieved the 'First Review' milestone by sharing your take on {game_name} game. Your opinions help others make great choices!"), + FIRST_VOTE_ACHIEVEMENT("Congratulations, {user_name}! 🗳️ You've earned the 'First Vote' badge by actively participating in community voting. Your valuable input significantly influences our community's development!"), + FIRST_COMMENT_ACHIEVEMENT("Great job, {user_name}! 💬 You've earned the 'First Comment' recognition by engaging in discussions. Your thoughts on '{post_title}' contribute to our community conversations!"), + NEW_POST_IN_PRIVATE_GROUP("Hey {user_name}! 🌟 There's something new for you. A fresh post has just landed in your private group, '{group_title}'. Check it out and join the conversation. We can't wait to hear your thoughts!"), + FIRST_VOTE_OF_THE_POST("Hey {user_name}! 📣 Your post '{post_title}' just got its first vote. Check it out and see what others think about your post!"), + NTH_VOTE_OF_THE_POST("Exciting news {user_name}! 📣 Your post '{post_title}' just reached {overall_vote} overall vote. It's great to see your contributions resonating with the community. Keep up the fantastic work and keep sharing your thoughts!"), + NEW_COMMENT_FOR_THE_POST("Good news, {user_name}! 🌟 Your post '{post_title}' has a new comment. It's great to see your contributions sparking conversations in the community. Dive in and join the discussion!"), + NEW_REPLY_FOR_THE_COMMENT("Hey {user_name}! 🌟 Your comment on '{post_title}' has a new reply. It's great to see your contributions sparking conversations in the community. Dive in and join the discussion!"), + BANNED_FROM_GROUP("Important Notice, {user_name}! 🚫 You have been banned from the '{group_title}' group. This may be due to a violation of our community guidelines or group rules. If you believe this is a mistake, you can contact the moderators for further clarification."), + APPLICATION_ACCEPTED("Congratulations, {user_name}! 🎉 Your application to join the private group '{group_title}' has been accepted. Welcome to the group! Dive in and start engaging with your new community."), + APPLICATION_REJECTED("Notification for {user_name}, 🚫 Your application to join the private group '{group_title}' has been reviewed and unfortunately, it has not been accepted at this time. This decision is based on our group criteria and community guidelines. We encourage you to review these guidelines and consider applying again in the future."), + NEW_GROUP_APPLICATION("Attention Moderators! 🌟 There is a new application for the private group '{group_title}'. Applicant: {user_name}. Please review the application and take appropriate action to maintain the integrity and quality of our group community."); + private final String messageTemplate; + NotificationMessage(String messageTemplate) { + this.messageTemplate = messageTemplate; + } +} diff --git a/app/backend/src/main/java/com/app/gamereview/enums/NotificationParent.java b/app/backend/src/main/java/com/app/gamereview/enums/NotificationParent.java new file mode 100644 index 00000000..1b73ad4e --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/enums/NotificationParent.java @@ -0,0 +1,9 @@ +package com.app.gamereview.enums; + +public enum NotificationParent { + POST, + COMMENT, + ACHIEVEMENT, + GROUP, + GROUP_APPLICATION +} diff --git a/app/backend/src/main/java/com/app/gamereview/model/Character.java b/app/backend/src/main/java/com/app/gamereview/model/Character.java new file mode 100644 index 00000000..f4e6ff0f --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/model/Character.java @@ -0,0 +1,48 @@ +package com.app.gamereview.model; + +import com.app.gamereview.model.common.BaseModel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +import java.util.List; +import java.util.Map; + +@Document(collection = "Character") +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class Character extends BaseModel { + private String name; + + private String icon; + + private String description; + + private List games; + + private String type; + + private String gender; + + private String race; + + private String status; + + private String occupation; + + private String birthDate; + + private String voiceActor; + + private String height; + + private String age; + + @Field("customFields") + private Map customFields; +} diff --git a/app/backend/src/main/java/com/app/gamereview/model/Game.java b/app/backend/src/main/java/com/app/gamereview/model/Game.java index 69a1e4ea..1989b47e 100644 --- a/app/backend/src/main/java/com/app/gamereview/model/Game.java +++ b/app/backend/src/main/java/com/app/gamereview/model/Game.java @@ -50,6 +50,8 @@ public class Game extends BaseModel { private String minSystemReq; + private Boolean isPromoted = false; + public Game(String gameName, String gameDescription, Date releaseDate, String minSystemReq) { this.gameName = gameName; this.gameDescription = gameDescription; diff --git a/app/backend/src/main/java/com/app/gamereview/model/Group.java b/app/backend/src/main/java/com/app/gamereview/model/Group.java index 94d4d07f..ff9b266b 100644 --- a/app/backend/src/main/java/com/app/gamereview/model/Group.java +++ b/app/backend/src/main/java/com/app/gamereview/model/Group.java @@ -27,6 +27,8 @@ public class Group extends BaseModel { private String forumId; // id of the forum of the group + private String groupIcon; + private int quota; private List moderators = new ArrayList<>(); // userIds of the moderators diff --git a/app/backend/src/main/java/com/app/gamereview/model/GroupApplication.java b/app/backend/src/main/java/com/app/gamereview/model/GroupApplication.java new file mode 100644 index 00000000..c55bfc6a --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/model/GroupApplication.java @@ -0,0 +1,37 @@ +package com.app.gamereview.model; + +import com.app.gamereview.enums.GroupApplicationStatus; +import com.app.gamereview.model.common.BaseModel; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.TypeAlias; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Document(collection = "GroupApplication") +@TypeAlias("GroupApplication") +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class GroupApplication extends BaseModel { + @NotNull + private String user; + + @NotNull + private String group; + + @NotNull + private String message; + + @NotNull + private GroupApplicationStatus status = GroupApplicationStatus.PENDING; + + private String reviewer; + + private LocalDateTime reviewedAt; +} diff --git a/app/backend/src/main/java/com/app/gamereview/model/Notification.java b/app/backend/src/main/java/com/app/gamereview/model/Notification.java new file mode 100644 index 00000000..3a24ba31 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/model/Notification.java @@ -0,0 +1,34 @@ +package com.app.gamereview.model; + +import com.app.gamereview.enums.NotificationParent; +import com.app.gamereview.model.common.BaseModel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document(collection = "Notification") +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class Notification extends BaseModel { + private String parent; + private NotificationParent parentType; + private String message; + private String user; + private Boolean isRead = false; + + public Notification(Notification notification) { + this.parent = notification.getParent(); + this.parentType = notification.getParentType(); + this.message = notification.getMessage(); + this.user = notification.getUser(); + this.isRead = notification.getIsRead(); + } + + public void setIsRead(Boolean isRead) { + this.isRead = isRead; + } +} diff --git a/app/backend/src/main/java/com/app/gamereview/model/Post.java b/app/backend/src/main/java/com/app/gamereview/model/Post.java index d5ef027d..39968600 100644 --- a/app/backend/src/main/java/com/app/gamereview/model/Post.java +++ b/app/backend/src/main/java/com/app/gamereview/model/Post.java @@ -51,6 +51,8 @@ public class Post extends BaseModel { private String achievement; + private String character; + public void addVote(VoteChoice choice){ voteCount += 1; if(choice.name().equals("UPVOTE")){ diff --git a/app/backend/src/main/java/com/app/gamereview/model/Profile.java b/app/backend/src/main/java/com/app/gamereview/model/Profile.java index e2c484d5..d0803508 100644 --- a/app/backend/src/main/java/com/app/gamereview/model/Profile.java +++ b/app/backend/src/main/java/com/app/gamereview/model/Profile.java @@ -1,7 +1,9 @@ package com.app.gamereview.model; import com.app.gamereview.model.common.BaseModel; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.mongodb.core.mapping.Document; @@ -13,6 +15,8 @@ @TypeAlias("Profile") @Getter @Setter +@AllArgsConstructor +@NoArgsConstructor public class Profile extends BaseModel { private String userId; // user the profile is linked to diff --git a/app/backend/src/main/java/com/app/gamereview/model/common/BaseModel.java b/app/backend/src/main/java/com/app/gamereview/model/common/BaseModel.java index a092fd97..5b49c930 100644 --- a/app/backend/src/main/java/com/app/gamereview/model/common/BaseModel.java +++ b/app/backend/src/main/java/com/app/gamereview/model/common/BaseModel.java @@ -7,11 +7,12 @@ import lombok.Setter; import java.time.LocalDateTime; +import java.util.Objects; import java.util.UUID; @Getter @Setter -public abstract class BaseModel { +public class BaseModel { @Id private String id; @@ -27,4 +28,17 @@ public BaseModel() { this.isDeleted = false; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BaseModel given = (BaseModel) o; + return id.equals(given.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + } diff --git a/app/backend/src/main/java/com/app/gamereview/repository/AchievementRepository.java b/app/backend/src/main/java/com/app/gamereview/repository/AchievementRepository.java index 90ebbaa3..41a40123 100644 --- a/app/backend/src/main/java/com/app/gamereview/repository/AchievementRepository.java +++ b/app/backend/src/main/java/com/app/gamereview/repository/AchievementRepository.java @@ -10,6 +10,8 @@ public interface AchievementRepository extends MongoRepository findByTitleAndIsDeletedFalse(String title); + Optional findByTitle(String title); + Optional findByIdAndIsDeletedFalse(String id); List findByGameAndIsDeletedFalse(String game); diff --git a/app/backend/src/main/java/com/app/gamereview/repository/CharacterRepository.java b/app/backend/src/main/java/com/app/gamereview/repository/CharacterRepository.java new file mode 100644 index 00000000..520c0112 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/repository/CharacterRepository.java @@ -0,0 +1,15 @@ +package com.app.gamereview.repository; + +import com.app.gamereview.model.Character; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; +import java.util.Optional; + +public interface CharacterRepository extends MongoRepository { + + Optional findByIdAndIsDeletedFalse(String id); + + List findByGamesContains(String game); + +} diff --git a/app/backend/src/main/java/com/app/gamereview/repository/ForumRepository.java b/app/backend/src/main/java/com/app/gamereview/repository/ForumRepository.java index 54240efb..9ea0a67d 100644 --- a/app/backend/src/main/java/com/app/gamereview/repository/ForumRepository.java +++ b/app/backend/src/main/java/com/app/gamereview/repository/ForumRepository.java @@ -4,6 +4,9 @@ import com.app.gamereview.model.Forum; +import java.util.Optional; + public interface ForumRepository extends MongoRepository { + Optional findByIdAndIsDeletedFalse(String id); } diff --git a/app/backend/src/main/java/com/app/gamereview/repository/GroupApplicationRepository.java b/app/backend/src/main/java/com/app/gamereview/repository/GroupApplicationRepository.java new file mode 100644 index 00000000..83b47e3d --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/repository/GroupApplicationRepository.java @@ -0,0 +1,14 @@ +package com.app.gamereview.repository; + +import com.app.gamereview.enums.GroupApplicationStatus; +import com.app.gamereview.model.GroupApplication; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; +import java.util.Optional; + +public interface GroupApplicationRepository extends MongoRepository { + Optional findByUserAndGroupAndStatus(String userId, String groupId, GroupApplicationStatus status); + + List findByGroupAndStatus(String groupId, GroupApplicationStatus groupApplicationStatus); +} diff --git a/app/backend/src/main/java/com/app/gamereview/repository/NotificationRepository.java b/app/backend/src/main/java/com/app/gamereview/repository/NotificationRepository.java new file mode 100644 index 00000000..786b5d56 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/repository/NotificationRepository.java @@ -0,0 +1,15 @@ +package com.app.gamereview.repository; + +import com.app.gamereview.model.Notification; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + + +@Repository +public interface NotificationRepository extends MongoRepository { + + Optional findByUserAndIsDeletedFalse(String userId); + Optional findByParentAndUser(String parent, String user); +} diff --git a/app/backend/src/main/java/com/app/gamereview/repository/PostRepository.java b/app/backend/src/main/java/com/app/gamereview/repository/PostRepository.java index 0aceb659..338ef89b 100644 --- a/app/backend/src/main/java/com/app/gamereview/repository/PostRepository.java +++ b/app/backend/src/main/java/com/app/gamereview/repository/PostRepository.java @@ -4,6 +4,12 @@ import com.app.gamereview.model.Post; +import java.util.List; +import java.util.Optional; + public interface PostRepository extends MongoRepository { + Optional findByIdAndIsDeletedFalse(String postId); + + List findByForumAndIsDeletedFalse(String forum); } diff --git a/app/backend/src/main/java/com/app/gamereview/service/AchievementService.java b/app/backend/src/main/java/com/app/gamereview/service/AchievementService.java index 5e07f0f9..92d0d87e 100644 --- a/app/backend/src/main/java/com/app/gamereview/service/AchievementService.java +++ b/app/backend/src/main/java/com/app/gamereview/service/AchievementService.java @@ -14,6 +14,7 @@ import com.app.gamereview.repository.ProfileRepository; import org.modelmapper.ModelMapper; import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestParam; import java.util.List; import java.util.Optional; @@ -127,27 +128,45 @@ public Achievement deleteAchievement(String achievementName, String gameName) { throw new ResourceNotFoundException("Achievement with the given name is not found."); } - Optional gameWithName = gameRepository.findByGameNameAndIsDeletedFalse(gameName); + Achievement achievementToDelete = null; - if (gameWithName.isEmpty()) { - throw new ResourceNotFoundException("Game with the given name is not found."); - } + if(gameName != null){ + Optional gameWithName = gameRepository.findByGameNameAndIsDeletedFalse(gameName); - Achievement achievementToDelete = null; + if (gameWithName.isEmpty()) { + throw new ResourceNotFoundException("Game with the given name is not found."); + } + + for (Achievement achievement : achievementWithName) { + Optional game = gameRepository.findByIdAndIsDeletedFalse(achievement.getGame()); + if (game.isPresent() && game.get().getGameName().equals(gameName)) { + achievementToDelete = achievement; + } + } - for (Achievement achievement : achievementWithName) { - Optional game = gameRepository.findByIdAndIsDeletedFalse(achievement.getGame()); - if (game.isPresent() && game.get().getGameName().equals(gameName)) { - achievementToDelete = achievement; + if (achievementToDelete == null) { + throw new ResourceNotFoundException("There is no achievement with the given name in the game."); } + } + else{ + Optional achievement = achievementRepository.findByTitle(achievementName); + + if(achievement.isEmpty() || achievement.get().getIsDeleted()){ + throw new ResourceNotFoundException("Meta achievement with the given name is not found"); + } + + if(achievement.get().getType() == AchievementType.GAME){ + throw new BadRequestException("Cannot delete game achievement without specifying the game"); + } + + achievementToDelete = achievement.get(); - if (achievementToDelete == null) { - throw new ResourceNotFoundException("There is no achievement with the given name in the game."); } achievementToDelete.setIsDeleted(true); + achievementRepository.save(achievementToDelete); return achievementToDelete; } diff --git a/app/backend/src/main/java/com/app/gamereview/service/CharacterService.java b/app/backend/src/main/java/com/app/gamereview/service/CharacterService.java new file mode 100644 index 00000000..add35f3b --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/service/CharacterService.java @@ -0,0 +1,208 @@ +package com.app.gamereview.service; + +import com.app.gamereview.dto.request.character.UpdateCharacterRequestDto; +import com.app.gamereview.exception.BadRequestException; +import com.app.gamereview.exception.ResourceNotFoundException; +import com.app.gamereview.model.Game; +import com.app.gamereview.model.Character; +import com.app.gamereview.repository.CharacterRepository; +import com.app.gamereview.repository.GameRepository; +import com.app.gamereview.dto.request.character.CreateCharacterRequestDto; +import org.modelmapper.ModelMapper; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class CharacterService { + + private final CharacterRepository characterRepository; + + private final GameRepository gameRepository; + + private final ModelMapper modelMapper; + + public CharacterService(CharacterRepository characterRepository, GameRepository gameRepository, ModelMapper modelMapper) { + this.characterRepository = characterRepository; + this.gameRepository = gameRepository; + this.modelMapper = modelMapper; + } + + public Character createCharacter(CreateCharacterRequestDto characterRequestDto) { + + List games = characterRequestDto.getGames(); + + + if (games.isEmpty()) { + throw new BadRequestException("A character should have at least one game."); + } + + for (String gameId : games) { + Optional gameOptional = gameRepository.findByIdAndIsDeletedFalse(gameId); + + if (gameOptional.isEmpty()) { + throw new ResourceNotFoundException("Game with the given is not found."); + } + } + + Character characterToCreate = modelMapper.map(characterRequestDto, Character.class); + + return characterRepository.save(characterToCreate); + } + + public Character updateCharacter(String id, UpdateCharacterRequestDto requestDto) { + Optional characterOptional = characterRepository.findByIdAndIsDeletedFalse(id); + + if (characterOptional.isEmpty()) { + throw new ResourceNotFoundException("Character with the given id is not found."); + } + + Character characterToUpdate = characterOptional.get(); + + if (requestDto.getName() != null) { + if (requestDto.getName().isBlank()) { + throw new BadRequestException("Character name cannot be blank."); + } + characterToUpdate.setName(requestDto.getName()); + } + + if (requestDto.getIcon() != null) { + if (requestDto.getIcon().isBlank()) { + throw new BadRequestException("Character icon cannot be blank."); + } + characterToUpdate.setIcon(requestDto.getIcon()); + } + + if (requestDto.getDescription() != null) { + if (requestDto.getDescription().isBlank()) { + throw new BadRequestException("Character description cannot be blank."); + } + characterToUpdate.setDescription(requestDto.getDescription()); + } + + if (requestDto.getGames() != null) { + + if (requestDto.getGames().isEmpty()) { + throw new BadRequestException("A character should have at least one game."); + } + + for (String gameId : requestDto.getGames()) { + Optional gameOptional = gameRepository.findByIdAndIsDeletedFalse(gameId); + + if (gameOptional.isEmpty()) { + throw new ResourceNotFoundException("Game with the given is not found."); + } + } + + characterToUpdate.setGames(requestDto.getGames()); + } + + if (requestDto.getType() != null) { + if (requestDto.getType().isBlank()) { + characterToUpdate.setType(null); + } else { + characterToUpdate.setType(requestDto.getType()); + } + } + + if (requestDto.getGender() != null) { + if (requestDto.getGender().isBlank()) { + characterToUpdate.setGender(null); + } else { + characterToUpdate.setGender(requestDto.getGender()); + } + } + + if (requestDto.getRace() != null) { + if (requestDto.getRace().isBlank()) { + characterToUpdate.setRace(null); + } else { + characterToUpdate.setRace(requestDto.getRace()); + } + } + + if (requestDto.getStatus() != null) { + if (requestDto.getStatus().isBlank()) { + characterToUpdate.setStatus(null); + } else { + characterToUpdate.setStatus(requestDto.getStatus()); + } + } + + if (requestDto.getOccupation() != null) { + if (requestDto.getOccupation().isBlank()) { + characterToUpdate.setOccupation(null); + } else { + characterToUpdate.setOccupation(requestDto.getOccupation()); + } + } + + if (requestDto.getBirthDate() != null) { + if (requestDto.getBirthDate().isBlank()) { + characterToUpdate.setBirthDate(null); + } else { + characterToUpdate.setBirthDate(requestDto.getBirthDate()); + } + } + + if (requestDto.getVoiceActor() != null) { + if (requestDto.getVoiceActor().isBlank()) { + characterToUpdate.setVoiceActor(null); + } else { + characterToUpdate.setVoiceActor(requestDto.getVoiceActor()); + } + } + + if (requestDto.getHeight() != null) { + if (requestDto.getHeight().isBlank()) { + characterToUpdate.setHeight(null); + } else { + characterToUpdate.setHeight(requestDto.getHeight()); + } + } + + if (requestDto.getAge() != null) { + if (requestDto.getAge().isBlank()) { + characterToUpdate.setAge(null); + } else { + characterToUpdate.setAge(requestDto.getAge()); + } + } + + if (requestDto.getCustomFields() != null) { + characterToUpdate.setCustomFields(requestDto.getCustomFields()); + } + + characterRepository.save(characterToUpdate); + return characterToUpdate; + } + + public Character deleteCharacter(String id) { + Optional characterOptional = characterRepository.findByIdAndIsDeletedFalse(id); + + if (characterOptional.isEmpty()) { + throw new ResourceNotFoundException("Character with the given id is not found."); + } + + Character characterToDelete = characterOptional.get(); + + characterToDelete.setIsDeleted(true); + + characterRepository.save(characterToDelete); + return characterToDelete; + } + + public List getGameCharacters(String gameId) { + + Optional gameOptional = gameRepository.findByIdAndIsDeletedFalse(gameId); + + if (gameOptional.isEmpty()) { + throw new ResourceNotFoundException("Game with the given is not found."); + } + + return characterRepository.findByGamesContains(gameId); + } + + +} diff --git a/app/backend/src/main/java/com/app/gamereview/service/CommentService.java b/app/backend/src/main/java/com/app/gamereview/service/CommentService.java index 7d8372f5..22f7f111 100644 --- a/app/backend/src/main/java/com/app/gamereview/service/CommentService.java +++ b/app/backend/src/main/java/com/app/gamereview/service/CommentService.java @@ -3,18 +3,14 @@ import com.app.gamereview.dto.request.comment.CreateCommentRequestDto; import com.app.gamereview.dto.request.comment.EditCommentRequestDto; import com.app.gamereview.dto.request.comment.ReplyCommentRequestDto; +import com.app.gamereview.dto.request.notification.CreateNotificationRequestDto; import com.app.gamereview.dto.request.post.GetPostListFilterRequestDto; import com.app.gamereview.dto.response.post.GetPostListResponseDto; -import com.app.gamereview.enums.SortDirection; -import com.app.gamereview.enums.SortType; -import com.app.gamereview.enums.UserRole; +import com.app.gamereview.enums.*; import com.app.gamereview.exception.BadRequestException; import com.app.gamereview.exception.ResourceNotFoundException; import com.app.gamereview.model.*; -import com.app.gamereview.repository.AchievementRepository; -import com.app.gamereview.repository.CommentRepository; -import com.app.gamereview.repository.PostRepository; -import com.app.gamereview.repository.ProfileRepository; +import com.app.gamereview.repository.*; import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Sort; @@ -35,19 +31,25 @@ public class CommentService { private final CommentRepository commentRepository; private final ProfileRepository profileRepository; private final AchievementRepository achievementRepository; + private final UserRepository userRepository; private final ModelMapper modelMapper; private final MongoTemplate mongoTemplate; + private final NotificationService notificationService; + @Autowired - public CommentService(PostRepository postRepository, CommentRepository commentRepository, + public CommentService(PostRepository postRepository, CommentRepository commentRepository, UserRepository userRepository, ProfileRepository profileRepository, AchievementRepository achievementRepository, - MongoTemplate mongoTemplate, ModelMapper modelMapper) { + MongoTemplate mongoTemplate, ModelMapper modelMapper, NotificationService notificationService) { this.postRepository = postRepository; this.commentRepository = commentRepository; this.profileRepository = profileRepository; this.achievementRepository = achievementRepository; + this.userRepository = userRepository; this.modelMapper = modelMapper; this.mongoTemplate = mongoTemplate; + this.notificationService = notificationService; + } @@ -72,6 +74,14 @@ public Comment createComment(CreateCommentRequestDto request, User user) { achievementRepository.findByIdAndIsDeletedFalse("af009796-6799-42d4-ae40-9adbb92657c4"); achievement.ifPresent(value -> profile.addAchievement(value.getId())); profile.setIsCommentedYet(true); + CreateNotificationRequestDto createNotificationRequestDto= new CreateNotificationRequestDto(); + String message = NotificationMessage.FIRST_COMMENT_ACHIEVEMENT.getMessageTemplate() + .replace("{user_name}", user.getUsername()) + .replace("{post_title}", post.get().getTitle()); + createNotificationRequestDto.setMessage(message); + createNotificationRequestDto.setParentType(NotificationParent.ACHIEVEMENT); + createNotificationRequestDto.setUser(user.getId()); + notificationService.createNotification(createNotificationRequestDto); } profile.setCommentCount(profile.getCommentCount() + 1); @@ -80,7 +90,25 @@ public Comment createComment(CreateCommentRequestDto request, User user) { Comment commentToCreate = modelMapper.map(request, Comment.class); commentToCreate.setCommenter(user.getId()); commentToCreate.setLastEditedAt(commentToCreate.getCreatedAt()); - return commentRepository.save(commentToCreate); + + Comment savedComment = commentRepository.save(commentToCreate); + + if(!user.getId().equals(post.get().getPoster())){ + CreateNotificationRequestDto createNotificationRequestDto= new CreateNotificationRequestDto(); + Optional postOwner = userRepository.findByIdAndIsDeletedFalse(post.get().getPoster()); + if (postOwner.isPresent()) { + String message = NotificationMessage.NEW_COMMENT_FOR_THE_POST.getMessageTemplate() + .replace("{user_name}", postOwner.get().getUsername()) + .replace("{post_title}", post.get().getTitle()); + createNotificationRequestDto.setMessage(message); + createNotificationRequestDto.setParentType(NotificationParent.POST); + createNotificationRequestDto.setParent(post.get().getId()); + createNotificationRequestDto.setUser(post.get().getPoster()); + notificationService.createNotification(createNotificationRequestDto); + } + } + + return savedComment; } public Comment replyComment(ReplyCommentRequestDto request, User user) { @@ -113,7 +141,27 @@ public Comment replyComment(ReplyCommentRequestDto request, User user) { commentToCreate.setCommenter(user.getId()); commentToCreate.setLastEditedAt(commentToCreate.getCreatedAt()); commentToCreate.setPost(parentComment.get().getPost()); - return commentRepository.save(commentToCreate); + + Comment savedComment = commentRepository.save(commentToCreate); + + if(!user.getId().equals(parentComment.get().getCommenter())){ + CreateNotificationRequestDto createNotificationRequestDto= new CreateNotificationRequestDto(); + Optional post = postRepository.findById(parentComment.get().getPost()); + if (post.isPresent()) { + Optional commentOwner = userRepository.findByIdAndIsDeletedFalse(parentComment.get().getCommenter()); + if (commentOwner.isPresent()) { + String message = NotificationMessage.NEW_REPLY_FOR_THE_COMMENT.getMessageTemplate() + .replace("{user_name}", commentOwner.get().getUsername()) + .replace("{post_title}", post.get().getTitle()); + createNotificationRequestDto.setMessage(message); + createNotificationRequestDto.setParentType(NotificationParent.COMMENT); + createNotificationRequestDto.setParent(post.get().getId()); + createNotificationRequestDto.setUser(parentComment.get().getCommenter()); + notificationService.createNotification(createNotificationRequestDto); + } + } + } + return savedComment; } public Comment editComment(String id, EditCommentRequestDto request, User user) { diff --git a/app/backend/src/main/java/com/app/gamereview/service/GameService.java b/app/backend/src/main/java/com/app/gamereview/service/GameService.java index cc561dce..95a1f558 100644 --- a/app/backend/src/main/java/com/app/gamereview/service/GameService.java +++ b/app/backend/src/main/java/com/app/gamereview/service/GameService.java @@ -1,59 +1,56 @@ package com.app.gamereview.service; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import com.app.gamereview.dto.request.game.CreateGameRequestDto; -import com.app.gamereview.dto.request.game.AddGameTagRequestDto; -import com.app.gamereview.dto.request.game.RemoveGameTagRequestDto; -import com.app.gamereview.dto.response.group.GetGroupResponseDto; +import com.app.gamereview.dto.request.game.*; +import com.app.gamereview.dto.response.game.GameDetailResponseDto; +import com.app.gamereview.dto.response.game.GetGameListResponseDto; import com.app.gamereview.dto.response.tag.AddGameTagResponseDto; import com.app.gamereview.dto.response.tag.GetAllTagsOfGameResponseDto; import com.app.gamereview.enums.ForumType; import com.app.gamereview.enums.TagType; import com.app.gamereview.exception.BadRequestException; import com.app.gamereview.exception.ResourceNotFoundException; -import com.app.gamereview.model.Forum; -import com.app.gamereview.model.Group; -import com.app.gamereview.model.Tag; -import com.app.gamereview.repository.ForumRepository; -import com.app.gamereview.repository.TagRepository; +import com.app.gamereview.model.*; +import com.app.gamereview.repository.*; import org.modelmapper.ModelMapper; import org.modelmapper.PropertyMap; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.MatchOperation; +import org.springframework.data.mongodb.core.aggregation.SampleOperation; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.stereotype.Service; -import com.app.gamereview.dto.request.game.GetGameListRequestDto; -import com.app.gamereview.dto.response.game.GetGameListResponseDto; -import com.app.gamereview.model.Game; -import com.app.gamereview.repository.GameRepository; -import com.app.gamereview.dto.response.game.GameDetailResponseDto; +import java.util.*; +import java.util.stream.Collectors; @Service public class GameService { private final GameRepository gameRepository; + private final UserRepository userRepository; private final TagRepository tagRepository; private final ForumRepository forumRepository; + private final ProfileRepository profileRepository; + private final MongoTemplate mongoTemplate; private final ModelMapper modelMapper; @Autowired public GameService( - GameRepository gameRepository, - MongoTemplate mongoTemplate, - TagRepository tagRepository, ForumRepository forumRepository, ModelMapper modelMapper) { + GameRepository gameRepository, UserRepository userRepository, + MongoTemplate mongoTemplate, TagRepository tagRepository, ForumRepository forumRepository, + ProfileRepository profileRepository, ModelMapper modelMapper) { this.gameRepository = gameRepository; + this.userRepository = userRepository; this.tagRepository = tagRepository; this.mongoTemplate = mongoTemplate; this.forumRepository = forumRepository; + this.profileRepository = profileRepository; this.modelMapper = modelMapper; modelMapper.addMappings(new PropertyMap() { @@ -71,8 +68,20 @@ protected void configure() { }); } - public List getGames(GetGameListRequestDto filter) { + public List getGames(GetGameListRequestDto filter) { + + MatchOperation matchStage = Aggregation.match(Criteria.where("isPromoted").is(true)); + SampleOperation sampleStage = Aggregation.sample(2); + + Aggregation aggregation = Aggregation.newAggregation(matchStage, sampleStage); + + List randomPromotedGames = new ArrayList<>(mongoTemplate.aggregate(aggregation, "Game", Game.class).getMappedResults()); + List randomPromotedGamesIds = randomPromotedGames.stream().map(Game::getId).collect(Collectors.toList()); + Query query = new Query(); + + query.addCriteria(Criteria.where("_id").nin(randomPromotedGamesIds)); + if(filter != null) { if (filter.getFindDeleted() == null || !filter.getFindDeleted()) { query.addCriteria(Criteria.where("isDeleted").is(false)); @@ -97,11 +106,26 @@ public List getGames(GetGameListRequestDto filter) { } } - return mongoTemplate.find(query, Game.class); + List gamesList = mongoTemplate.find(query, Game.class); + + randomPromotedGames.addAll(gamesList); + + return randomPromotedGames.stream().map(this::mapToGetGameListResponseDto).collect(Collectors.toList()); + } public List getAllGames(GetGameListRequestDto filter) { + MatchOperation matchStage = Aggregation.match(Criteria.where("isPromoted").is(true)); + SampleOperation sampleStage = Aggregation.sample(2); + + Aggregation aggregation = Aggregation.newAggregation(matchStage, sampleStage); + + List randomPromotedGames = new ArrayList<>(mongoTemplate.aggregate(aggregation, "Game", Game.class).getMappedResults()); + List randomPromotedGamesIds = randomPromotedGames.stream().map(Game::getId).collect(Collectors.toList()); + Query query = new Query(); + + query.addCriteria(Criteria.where("_id").nin(randomPromotedGamesIds)); if(filter != null) { if (filter.getFindDeleted() == null || !filter.getFindDeleted()) { query.addCriteria(Criteria.where("isDeleted").is(false)); @@ -131,7 +155,9 @@ public List getAllGames(GetGameListRequestDto filter) { List gamesList = mongoTemplate.find(query, Game.class); - return gamesList.stream().map(this::mapToGetGameListResponseDto).collect(Collectors.toList()); + randomPromotedGames.addAll(gamesList); + + return randomPromotedGames.stream().map(this::mapToGetGameListResponseDto).collect(Collectors.toList()); } private GetGameListResponseDto mapToGetGameListResponseDto(Game game) { @@ -330,5 +356,193 @@ public Game createGame(CreateGameRequestDto request){ return gameRepository.save(gameToCreate); } + public Game editGame(String id, UpdateGameRequestDto request){ + Optional findGame = gameRepository.findByIdAndIsDeletedFalse(id); + + if(findGame.isEmpty()){ + throw new ResourceNotFoundException("Game is not found"); + } + + Game gameToUpdate = findGame.get(); + gameToUpdate.setGameName(request.getGameName()); + gameToUpdate.setGameDescription(request.getGameDescription()); + gameToUpdate.setGameIcon(request.getGameIcon()); + gameToUpdate.setReleaseDate(request.getReleaseDate()); + gameToUpdate.setMinSystemReq(request.getMinSystemReq()); + gameRepository.save(gameToUpdate); + + return gameToUpdate; + } + + public Boolean deleteGame(String id){ + Optional findGame = gameRepository.findByIdAndIsDeletedFalse(id); + + if(findGame.isEmpty()){ + throw new ResourceNotFoundException("Game is not found"); + } + + Game gameToDelete = findGame.get(); + gameToDelete.setIsDeleted(true); + gameRepository.save(gameToDelete); + + return true; + } + + public List getRecommendedGames(){ + Query query = new Query(); // all games except the base game + query.addCriteria(Criteria.where("isDeleted").is(false)); + query.with(Sort.by(Sort.Direction.DESC, "overallVote")); + query.limit(10); + + return mongoTemplate.find(query, Game.class); + } + + public List getRecommendedGames(String email){ + + if(email == null) throw new ResourceNotFoundException("User's token couldn't be validated"); + + Optional findUser = userRepository.findByEmailAndIsDeletedFalse(email); + + if(findUser.isEmpty()) throw new ResourceNotFoundException( + "User with the token/email information couldn't be found"); + + User user = findUser.get(); + + Optional findProfile = profileRepository.findByUserIdAndIsDeletedFalse(user.getId()); + + if(findProfile.isEmpty()){ + throw new ResourceNotFoundException("Profile of the user is not found, unexpected error has occurred"); + } + + Profile profile = findProfile.get(); + List followedGameIds = profile.getGames(); + + if(followedGameIds.isEmpty()){ + return getRecommendedGames(); + } + + TreeSet recommendedGames = new TreeSet<>(Comparator.reverseOrder()); + + for(String gameId : followedGameIds){ + recommendedGames.addAll(recommendationByGameId(gameId)); + } + + List recommendations = new ArrayList<>(); + + for(RecommendGameDto gameDto : recommendedGames){ + Game gameToRecommend = gameDto.getGame(); + if(!followedGameIds.contains(gameToRecommend.getId())){ + recommendations.add(gameToRecommend); + } + if(recommendations.size() >= 10){ // get only top 10 recommendations + break; + } + } + + if(recommendations.size() < 10){ + int diff = 10; + Query query = new Query(); // all games except the base game + query.addCriteria(Criteria.where("isDeleted").is(false)); + query.with(Sort.by(Sort.Direction.DESC, "overallVote")); + query.limit(diff); + List extraGames = mongoTemplate.find(query, Game.class); + + for(Game game : extraGames){ + if(!recommendations.contains(game)) + recommendations.add(game); + } + + return recommendations; + } + + return recommendations; + } + + public TreeSet recommendationByGameId(String gameId){ + Optional findGame = gameRepository.findByIdAndIsDeletedFalse(gameId); + + Set idList = new HashSet<>(); + + if(findGame.isEmpty()){ + throw new ResourceNotFoundException("Game is not found"); + } + + Game game = findGame.get(); + idList.add(game.getId()); + + TreeSet scoreSet = new TreeSet<>(Comparator.reverseOrder()); + + String[] words = game.getGameName().split(" ",-2); + for(String word : words){ + if(word.length() > 3){ + String regexPattern = ".*" + word + ".*"; + Query query = new Query(); + query.addCriteria(Criteria.where("gameName").regex(regexPattern, "i")); + query.addCriteria(Criteria.where("_id").ne(game.getId())); + List similarNames = mongoTemplate.find(query, Game.class); + for(Game i : similarNames){ + RecommendGameDto dto = new RecommendGameDto(); + dto.setGame(i); + scoreSet.add(dto); + idList.add(dto.getGame().getId()); + } + } + } + // List recommendations = new ArrayList<>(); + + Query allGamesQuery = new Query(); // all games except the base game + allGamesQuery.addCriteria(Criteria.where("isDeleted").is(false)); + allGamesQuery.addCriteria(Criteria.where("_id").nin(idList)); + + List allGames = mongoTemplate.find(allGamesQuery, Game.class); + + for(Game candidateGame : allGames){ + int score = calculateSimilarityScore(game, candidateGame); + if(score != 0){ + RecommendGameDto dto = new RecommendGameDto(); + dto.setGame(candidateGame); + dto.setScore(score); + scoreSet.add(dto); + } + } + + return scoreSet; + } + + public int calculateSimilarityScore(Game based, Game candidate){ + int score = 0; + + List baseTags = based.getAllTags(); + baseTags.retainAll(candidate.getAllTags()); + + for(String tagId : baseTags){ + Optional findTag = tagRepository.findByIdAndIsDeletedFalse(tagId); + if(findTag.isPresent()){ + score++; + if(findTag.get().getTagType().equals(TagType.PRODUCTION)){ + score += 2; + } + else if(findTag.get().getTagType().equals(TagType.GENRE)){ + score += 4; + } + } + } + return score; + } + + public Game changePromotionStatusOfGame(String id){ + Optional findGame = gameRepository.findByIdAndIsDeletedFalse(id); + + if(findGame.isEmpty()) { + throw new ResourceNotFoundException("Game is not found"); + } + + Game game = findGame.get(); + game.setIsPromoted(!game.getIsPromoted()); + gameRepository.save(game); + return game; + } } + + diff --git a/app/backend/src/main/java/com/app/gamereview/service/GroupService.java b/app/backend/src/main/java/com/app/gamereview/service/GroupService.java index 02628416..e52a7d91 100644 --- a/app/backend/src/main/java/com/app/gamereview/service/GroupService.java +++ b/app/backend/src/main/java/com/app/gamereview/service/GroupService.java @@ -1,8 +1,10 @@ package com.app.gamereview.service; import com.app.gamereview.dto.request.group.*; +import com.app.gamereview.dto.request.notification.CreateNotificationRequestDto; import com.app.gamereview.dto.response.group.GetGroupDetailResponseDto; import com.app.gamereview.dto.response.group.GetGroupResponseDto; +import com.app.gamereview.dto.response.group.GroupApplicationResponseDto; import com.app.gamereview.dto.response.tag.AddGroupTagResponseDto; import com.app.gamereview.enums.*; import com.app.gamereview.exception.BadRequestException; @@ -19,6 +21,7 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.*; @Service @@ -35,10 +38,13 @@ public class GroupService { private final UserRepository userRepository; private final ProfileRepository profileRepository; + private final GroupApplicationRepository groupApplicationRepository; private final MongoTemplate mongoTemplate; private final ModelMapper modelMapper; + private final GameService gameService; + private final NotificationService notificationService; @Autowired public GroupService( @@ -48,8 +54,11 @@ public GroupService( TagRepository tagRepository, UserRepository userRepository, ProfileRepository profileRepository, + GroupApplicationRepository groupApplicationRepository, MongoTemplate mongoTemplate, - ModelMapper modelMapper + ModelMapper modelMapper, + GameService gameService, + NotificationService notificationService ) { this.groupRepository = groupRepository; this.gameRepository = gameRepository; @@ -57,8 +66,11 @@ public GroupService( this.tagRepository = tagRepository; this.userRepository = userRepository; this.profileRepository = profileRepository; + this.groupApplicationRepository = groupApplicationRepository; this.mongoTemplate = mongoTemplate; this.modelMapper = modelMapper; + this.gameService = gameService; + this.notificationService = notificationService; modelMapper.addMappings(new PropertyMap() { @Override @@ -86,7 +98,7 @@ protected void configure() { }); } - public List getAllGroups(GetAllGroupsFilterRequestDto filter, String email){ + public List getAllGroups(GetAllGroupsFilterRequestDto filter, String email) { Optional loggedInUser = userRepository.findByEmailAndIsDeletedFalse(email); String loggedInUserId = loggedInUser.map(User::getId).orElse(null); @@ -94,7 +106,7 @@ public List getAllGroups(GetAllGroupsFilterRequestDto filte Query query = new Query(); // search for title - if(filter.getTitle() != null && !filter.getTitle().isBlank()){ + if (filter.getTitle() != null && !filter.getTitle().isBlank()) { String regexPattern = ".*" + filter.getTitle() + ".*"; query.addCriteria(Criteria.where("title").regex(regexPattern, "i")); } @@ -107,7 +119,7 @@ public List getAllGroups(GetAllGroupsFilterRequestDto filte if (filter.getGameName() != null && !filter.getGameName().isBlank()) { String gameName = filter.getGameName(); Optional game = gameRepository.findByGameNameAndIsDeletedFalse(gameName); - if(game.isEmpty()){ + if (game.isEmpty()) { return new ArrayList<>(); } query.addCriteria(Criteria.where("gameId").is(game.get().getId())); @@ -123,19 +135,18 @@ public List getAllGroups(GetAllGroupsFilterRequestDto filte } if (filter.getSortBy().equals(SortType.CREATION_DATE.name())) { query.with(Sort.by(sortDirection, "createdAt")); - } - else if (filter.getSortBy().equals(SortType.QUOTA.name())) { + } else if (filter.getSortBy().equals(SortType.QUOTA.name())) { query.with(Sort.by(sortDirection, "quota")); } } - List filteredGroups = mongoTemplate.find(query,Group.class); + List filteredGroups = mongoTemplate.find(query, Group.class); List responseDtos = new ArrayList<>(); - for(Group group : filteredGroups){ - GetGroupResponseDto dto = modelMapper.map(group,GetGroupResponseDto.class); + for (Group group : filteredGroups) { + GetGroupResponseDto dto = modelMapper.map(group, GetGroupResponseDto.class); - for(String tagId : group.getTags()){ + for (String tagId : group.getTags()) { Optional tag = tagRepository.findById(tagId); tag.ifPresent(dto::populateTag); } @@ -148,21 +159,21 @@ else if (filter.getSortBy().equals(SortType.QUOTA.name())) { return responseDtos; } - public GetGroupDetailResponseDto getGroupById(String groupId, String email){ + public GetGroupDetailResponseDto getGroupById(String groupId, String email) { Optional loggedInUser = userRepository.findByEmailAndIsDeletedFalse(email); String loggedInUserId = loggedInUser.map(User::getId).orElse(null); Optional isGroupExists = groupRepository.findByIdAndIsDeletedFalse(groupId); - if(isGroupExists.isEmpty()){ + if (isGroupExists.isEmpty()) { throw new ResourceNotFoundException("Group not found"); } Group group = isGroupExists.get(); GetGroupDetailResponseDto dto = modelMapper.map(group, GetGroupDetailResponseDto.class); - for(String tagId : group.getTags()){ + for (String tagId : group.getTags()) { Optional tag = tagRepository.findById(tagId); tag.ifPresent(dto::populateTag); } @@ -176,15 +187,15 @@ public GetGroupDetailResponseDto getGroupById(String groupId, String email){ return dto; } - public List mapMemberIdsToMemberInfos(List memberIds){ + public List mapMemberIdsToMemberInfos(List memberIds) { List memberInfos = new ArrayList<>(); - for(String memberId : memberIds){ + for (String memberId : memberIds) { Optional profileOfMember = profileRepository.findByUserIdAndIsDeletedFalse(memberId); Optional user = userRepository.findByIdAndIsDeletedFalse(memberId); - if(user.isEmpty() || profileOfMember.isEmpty()) + if (user.isEmpty() || profileOfMember.isEmpty()) continue; GetGroupDetailResponseDto.MemberInfo memberInfo = new GetGroupDetailResponseDto.MemberInfo(); @@ -197,29 +208,29 @@ public List mapMemberIdsToMemberInfos(List return memberInfos; } - public Group createGroup(CreateGroupRequestDto request, User user){ + public Group createGroup(CreateGroupRequestDto request, User user) { Optional sameTitle = groupRepository.findByTitleAndIsDeletedFalse(request.getTitle()); - if(sameTitle.isPresent()){ + if (sameTitle.isPresent()) { throw new BadRequestException("Group with same title already exists, please pick a new title"); } - if(request.getTags() != null){ - for(String tagId : request.getTags()){ + if (request.getTags() != null) { + for (String tagId : request.getTags()) { Optional tag = tagRepository.findByIdAndIsDeletedFalse(tagId); - if(tag.isEmpty()){ + if (tag.isEmpty()) { throw new ResourceNotFoundException("One of the added tag is not found"); } - if(!tag.get().getTagType().name().equals(TagType.GROUP.name())){ + if (!tag.get().getTagType().name().equals(TagType.GROUP.name())) { throw new BadRequestException("Groups can only be tagged with group tags"); } } } - if(request.getGameId() != null){ + if (request.getGameId() != null) { Optional game = gameRepository.findByIdAndIsDeletedFalse(request.getGameId()); - if(game.isEmpty()){ + if (game.isEmpty()) { throw new ResourceNotFoundException("Game is not found"); } } @@ -242,17 +253,16 @@ public Group createGroup(CreateGroupRequestDto request, User user){ return groupRepository.save(groupToCreate); } - public Boolean deleteGroup(String identifier){ + public Boolean deleteGroup(String identifier) { Optional foundGroup; - if(UtilExtensions.isUUID(identifier)){ + if (UtilExtensions.isUUID(identifier)) { foundGroup = groupRepository.findByIdAndIsDeletedFalse(identifier); - } - else{ + } else { foundGroup = groupRepository.findByTitleAndIsDeletedFalse(identifier); } - if(foundGroup.isEmpty()){ + if (foundGroup.isEmpty()) { throw new ResourceNotFoundException("Group is not found"); } @@ -263,16 +273,16 @@ public Boolean deleteGroup(String identifier){ return true; } - public Group updateGroup(String groupId, UpdateGroupRequestDto request){ + public Group updateGroup(String groupId, UpdateGroupRequestDto request) { Optional foundGroup = groupRepository.findByIdAndIsDeletedFalse(groupId); - if(foundGroup.isEmpty()){ + if (foundGroup.isEmpty()) { throw new ResourceNotFoundException("Group does not exist"); } Group groupToUpdate = foundGroup.get(); - if(groupToUpdate.getMembers().size() > request.getQuota()){ + if (groupToUpdate.getMembers().size() > request.getQuota()) { throw new BadRequestException("Quota cannot be less than the current number of members in group"); } @@ -282,31 +292,32 @@ public Group updateGroup(String groupId, UpdateGroupRequestDto request){ groupToUpdate.setMembershipPolicy(MembershipPolicy.valueOf(request.getMembershipPolicy())); groupToUpdate.setQuota(request.getQuota()); groupToUpdate.setAvatarOnly(request.getAvatarOnly()); + groupToUpdate.setGroupIcon(request.getGroupIcon()); groupRepository.save(groupToUpdate); return groupToUpdate; } - public AddGroupTagResponseDto addGroupTag(AddGroupTagRequestDto request){ + public AddGroupTagResponseDto addGroupTag(AddGroupTagRequestDto request) { Optional findGroup = groupRepository.findById(request.getGroupId()); Optional findTag = tagRepository.findById(request.getTagId()); - if(findGroup.isEmpty() || findGroup.get().getIsDeleted()){ + if (findGroup.isEmpty() || findGroup.get().getIsDeleted()) { throw new ResourceNotFoundException("Group does not exist"); } - if(findTag.isEmpty() || findTag.get().getIsDeleted()){ + if (findTag.isEmpty() || findTag.get().getIsDeleted()) { throw new ResourceNotFoundException("Tag does not exist"); } - if(!findTag.get().getTagType().name().equals(TagType.GROUP.name())){ + if (!findTag.get().getTagType().name().equals(TagType.GROUP.name())) { throw new BadRequestException("Only GROUP tags can be added to groups"); } Group group = findGroup.get(); Tag tag = findTag.get(); - if(group.getTags().contains(tag.getId())){ + if (group.getTags().contains(tag.getId())) { throw new BadRequestException("Tag is already added"); } @@ -319,23 +330,23 @@ public AddGroupTagResponseDto addGroupTag(AddGroupTagRequestDto request){ return response; } - public Boolean removeGroupTag(RemoveGroupTagRequestDto request){ + public Boolean removeGroupTag(RemoveGroupTagRequestDto request) { Optional findGroup = groupRepository.findById(request.getGroupId()); Optional findTag = tagRepository.findById(request.getTagId()); - if(findGroup.isEmpty() || findGroup.get().getIsDeleted()){ + if (findGroup.isEmpty() || findGroup.get().getIsDeleted()) { throw new ResourceNotFoundException("Group does not exist"); } - if(findTag.isEmpty() || findTag.get().getIsDeleted()){ + if (findTag.isEmpty() || findTag.get().getIsDeleted()) { throw new ResourceNotFoundException("Tag does not exist"); } Group group = findGroup.get(); Tag tag = findTag.get(); - if(!group.getTags().contains(tag.getId())){ + if (!group.getTags().contains(tag.getId())) { return false; } @@ -345,18 +356,18 @@ public Boolean removeGroupTag(RemoveGroupTagRequestDto request){ return true; } - public Boolean joinGroup(String groupId, User user){ + public Boolean joinGroup(String groupId, User user) { Optional isGroupExists = groupRepository.findByIdAndIsDeletedFalse(groupId); - if(isGroupExists.isEmpty()){ + if (isGroupExists.isEmpty()) { throw new ResourceNotFoundException("Group not found"); } - if(MembershipPolicy.PRIVATE.equals(isGroupExists.get().getMembershipPolicy())){ + if (MembershipPolicy.PRIVATE.equals(isGroupExists.get().getMembershipPolicy())) { throw new BadRequestException("You should send join request for this group"); } - if(isGroupExists.get().getQuota() <= isGroupExists.get().getMembers().size()){ + if (isGroupExists.get().getQuota() <= isGroupExists.get().getMembers().size()) { throw new BadRequestException("You cannot join, group is full"); } @@ -366,10 +377,10 @@ public Boolean joinGroup(String groupId, User user){ return true; } - public Boolean leaveGroup(String groupId, User user){ + public Boolean leaveGroup(String groupId, User user) { Optional isGroupExists = groupRepository.findByIdAndIsDeletedFalse(groupId); - if(isGroupExists.isEmpty()){ + if (isGroupExists.isEmpty()) { throw new ResourceNotFoundException("Group not found"); } @@ -398,6 +409,20 @@ public Boolean banUser(String groupId, String userId, User user) { group.get().addBannedUser(userId); groupRepository.save(group.get()); + Optional optionalBannedUser = userRepository.findByIdAndIsDeletedFalse(userId); + if (optionalBannedUser.isPresent()) { + User bannedUser = optionalBannedUser.get(); + CreateNotificationRequestDto createNotificationRequestDto = new CreateNotificationRequestDto(); + String message = NotificationMessage.BANNED_FROM_GROUP.getMessageTemplate() + .replace("{user_name}", bannedUser.getUsername()) + .replace("{group_title}", group.get().getTitle()); + createNotificationRequestDto.setMessage(message); + createNotificationRequestDto.setParentType(NotificationParent.GROUP); + createNotificationRequestDto.setParent(group.get().getId()); + createNotificationRequestDto.setUser(bannedUser.getId()); + notificationService.createNotification(createNotificationRequestDto); + } + return true; } @@ -448,5 +473,313 @@ public Boolean unbanUser(String groupId, String userId, User user) { return true; } + public Boolean applyGroup(String groupId, User user, GroupApplicationRequestDto dto) { + Optional isGroupExists = groupRepository.findByIdAndIsDeletedFalse(groupId); + + if (isGroupExists.isEmpty()) { + throw new ResourceNotFoundException("Group not found"); + } + Group group = isGroupExists.get(); + + if (MembershipPolicy.PUBLIC.equals(group.getMembershipPolicy())) { + throw new BadRequestException("This group is public, you can't apply to this group"); + } + + if (group.getMembers().contains(user.getId())) { + throw new BadRequestException("You are already a member of this group so you can't apply."); + } + + if (group.getBannedMembers().contains(user.getId())) { + throw new BadRequestException("You are banned from this group so you can't apply."); + } + + Optional groupJoinRequest = groupApplicationRepository.findByUserAndGroupAndStatus(user.getId(), groupId, GroupApplicationStatus.PENDING); + + if (groupJoinRequest.isPresent()) { + throw new BadRequestException("You have a pending request for this group, please wait for the response."); + } + + GroupApplication request = new GroupApplication(); + request.setUser(user.getId()); + request.setGroup(groupId); + request.setMessage(dto.getMessage()); + request.setStatus(GroupApplicationStatus.PENDING); + groupApplicationRepository.save(request); + + // send notification to moderators + List moderators = group.getModerators(); + for (String moderatorId : moderators) { + Optional optionalModerator = userRepository.findByIdAndIsDeletedFalse(moderatorId); + if (optionalModerator.isPresent()) { + User moderator = optionalModerator.get(); + CreateNotificationRequestDto createNotificationRequestDto = new CreateNotificationRequestDto(); + String message = NotificationMessage.NEW_GROUP_APPLICATION.getMessageTemplate() + .replace("{user_name}", user.getUsername()) + .replace("{group_title}", group.getTitle()); + createNotificationRequestDto.setMessage(message); + createNotificationRequestDto.setParentType(NotificationParent.GROUP_APPLICATION); + createNotificationRequestDto.setParent(group.getId()); + createNotificationRequestDto.setUser(moderator.getId()); + notificationService.createNotification(createNotificationRequestDto); + } + } + return true; + } + + public Boolean reviewApplication(String applicationId, User user, GroupApplicationReviewDto dto) { + Optional optionalApplication = groupApplicationRepository.findById(applicationId); + + if (optionalApplication.isEmpty()) { + throw new ResourceNotFoundException("Application not found"); + } + GroupApplication application = optionalApplication.get(); + + if (!application.getStatus().equals(GroupApplicationStatus.PENDING)) { + throw new BadRequestException("Application is already reviewed"); + } + + Optional optionalGroup = groupRepository.findByIdAndIsDeletedFalse(application.getGroup()); + if (optionalGroup.isEmpty()) { + throw new ResourceNotFoundException("Applied group is not found"); + } + Group appliedGroup = optionalGroup.get(); + + if (!appliedGroup.getModerators().contains(user.getId())) { + throw new BadRequestException("You are not a moderator of this group"); + } + + if (GroupApplicationReviewResult.APPROVE.name().equals(dto.getResult())) { + if (appliedGroup.getMembers().size() >= appliedGroup.getQuota()) { + throw new BadRequestException("Group is full"); + } + if (appliedGroup.getBannedMembers().contains(application.getUser())) { + throw new BadRequestException("User is banned from this group"); + } + if (!appliedGroup.getMembers().contains(application.getUser())) { + appliedGroup.addMember(application.getUser()); + groupRepository.save(appliedGroup); + } + application.setStatus(GroupApplicationStatus.APPROVED); + application.setReviewer(user.getId()); + application.setReviewedAt(LocalDateTime.now()); + groupApplicationRepository.save(application); + + // send APPLICATION_ACCEPTED notification to user + Optional optionalUser = userRepository.findByIdAndIsDeletedFalse(application.getUser()); + if (optionalUser.isPresent()) { + User appliedUser = optionalUser.get(); + CreateNotificationRequestDto createNotificationRequestDto = new CreateNotificationRequestDto(); + String message = NotificationMessage.APPLICATION_ACCEPTED.getMessageTemplate() + .replace("{user_name}", appliedUser.getUsername()) + .replace("{group_title}", appliedGroup.getTitle()); + createNotificationRequestDto.setMessage(message); + createNotificationRequestDto.setParentType(NotificationParent.GROUP); + createNotificationRequestDto.setParent(application.getGroup()); + createNotificationRequestDto.setUser(appliedUser.getId()); + notificationService.createNotification(createNotificationRequestDto); + } + + } else if (GroupApplicationReviewResult.REJECT.name().equals(dto.getResult())) { + application.setStatus(GroupApplicationStatus.REJECTED); + application.setReviewer(user.getId()); + application.setReviewedAt(LocalDateTime.now()); + groupApplicationRepository.save(application); + + // send APPLICATION_REJECTED notification to user + Optional optionalUser = userRepository.findByIdAndIsDeletedFalse(application.getUser()); + if (optionalUser.isPresent()) { + User appliedUser = optionalUser.get(); + CreateNotificationRequestDto createNotificationRequestDto = new CreateNotificationRequestDto(); + String message = NotificationMessage.APPLICATION_REJECTED.getMessageTemplate() + .replace("{user_name}", appliedUser.getUsername()) + .replace("{group_title}", appliedGroup.getTitle()); + createNotificationRequestDto.setMessage(message); + createNotificationRequestDto.setParentType(NotificationParent.GROUP); + createNotificationRequestDto.setParent(application.getGroup()); + createNotificationRequestDto.setUser(appliedUser.getId()); + notificationService.createNotification(createNotificationRequestDto); + } + } else { + throw new BadRequestException("Invalid review result"); + } + return true; + } + + public List listApplications(String groupId, User user) { + Optional isGroupExists = groupRepository.findByIdAndIsDeletedFalse(groupId); + + if (isGroupExists.isEmpty()) { + throw new ResourceNotFoundException("Group not found"); + } + Group group = isGroupExists.get(); + + if (!group.getModerators().contains(user.getId())) { + throw new BadRequestException("You are not a moderator of this group"); + } + + List applications = groupApplicationRepository.findByGroupAndStatus(groupId, GroupApplicationStatus.PENDING); + // for each application, convert to dto and append in a list + List responseDtos = new ArrayList<>(); + for (GroupApplication application : applications) { + responseDtos.add(contertToDto(application)); + } + return responseDtos; + } + + private GroupApplicationResponseDto contertToDto(GroupApplication application) { + GroupApplicationResponseDto dto = new GroupApplicationResponseDto(); + dto.setId(application.getId()); + dto.setAppliedAt(application.getCreatedAt()); + + userRepository.findById(application.getUser()).ifPresent(dto::setApplicant); + groupRepository.findById(application.getGroup()).ifPresent(dto::setGroup); + + dto.setMessage(application.getMessage()); + dto.setStatus(application.getStatus()); + return dto; + } + + public List getRecommendedGroups(String email) { + Optional optUser = userRepository.findByEmailAndIsDeletedFalse(email); + + if(optUser.isEmpty()){ + Query query = new Query(); + query.addCriteria(Criteria.where("isDeleted").is(false)); + query.limit(10); + return mongoTemplate.find(query, Group.class); + } + User user = optUser.get(); + + Optional findProfile = profileRepository.findByUserIdAndIsDeletedFalse(user.getId()); + if (findProfile.isEmpty()) { + throw new ResourceNotFoundException("Profile of the user is not found, unexpected error has occurred"); + } + List recommendations = new ArrayList<>(); + List memberGroups = groupRepository.findUserGroups(user.getId()); + List userGames = findProfile.get().getGames(); + if (memberGroups.isEmpty()) { + if (userGames.isEmpty()){ + Query query = new Query(); + query.addCriteria(Criteria.where("isDeleted").is(false)); + query.limit(10); + return mongoTemplate.find(query, Group.class); + }else{ + List recommendationGameGroups = new ArrayList<>(); + for(String gameId : userGames){ + Query query = new Query(); + query.addCriteria(Criteria.where("isDeleted").is(false)); + query.addCriteria(Criteria.where("gameId").is(gameId)); + query.limit(2); + List gameGroups = mongoTemplate.find(query, Group.class); + recommendationGameGroups.addAll(gameGroups); + Collections.shuffle(recommendationGameGroups); + } + return recommendationGameGroups.subList(0, Math.min(10, recommendationGameGroups.size())); + } + } + TreeSet recommendedGroups = new TreeSet<>(Comparator.reverseOrder()); + for (Group group : memberGroups) { + String groupId = group.getId(); + recommendedGroups.addAll(recommendationByGroupId(groupId, user.getId())); + } + if(!userGames.isEmpty()){ + List recommendedGameGroups = new ArrayList<>(); + for(String gameId : userGames){ + Query query = new Query(); + query.addCriteria(Criteria.where("isDeleted").is(false)); + query.addCriteria(Criteria.where("gameId").is(gameId)); + query.addCriteria(Criteria.where("members").nin(user.getId())); + query.limit(2); + List gameGroups = mongoTemplate.find(query, Group.class); + recommendedGameGroups.addAll(gameGroups); + } + recommendations.addAll(recommendedGameGroups); + } + for (RecommendGroupDto groupDto : recommendedGroups) { + recommendations.add(groupDto.getGroup()); + } + + return recommendations.subList(0, Math.min(10, recommendations.size())); + } + + public TreeSet recommendationByGroupId(String groupId, String userId) { + Optional findGroup = groupRepository.findByIdAndIsDeletedFalse(groupId); + + Set idList = new HashSet<>(); + + if (findGroup.isEmpty()) { + throw new ResourceNotFoundException("Group is not found"); + } + + Group group = findGroup.get(); + idList.add(group.getId()); + + TreeSet scoreSet = new TreeSet<>(Comparator.reverseOrder()); + + String[] words = group.getTitle().split(" ", -2); + for (String word : words) { + if (word.length() > 3) { + String regexPattern = ".*" + word + ".*"; + Query query = new Query(); + query.addCriteria(Criteria.where("title").regex(regexPattern, "i")); + query.addCriteria(Criteria.where("_id").ne(group.getId())); + query.addCriteria(Criteria.where("members").nin(userId)); + List similarNames = mongoTemplate.find(query, Group.class); + for (Group i : similarNames) { + RecommendGroupDto dto = new RecommendGroupDto(); + dto.setGroup(i); + scoreSet.add(dto); + idList.add(dto.getGroup().getId()); + } + } + } + + Query allGroupsQuery = new Query(); + allGroupsQuery.addCriteria(Criteria.where("isDeleted").is(false)); + allGroupsQuery.addCriteria(Criteria.where("_id").nin(idList)); + allGroupsQuery.addCriteria(Criteria.where("members").nin(userId)); + + List allGroups = mongoTemplate.find(allGroupsQuery, Group.class); + + for (Group candidateGroup : allGroups) { + int score = calculateGroupSimilarityScore(group, candidateGroup); + if (score != 0) { + RecommendGroupDto dto = new RecommendGroupDto(); + dto.setGroup(candidateGroup); + dto.setScore(score); + scoreSet.add(dto); + } + } + + return scoreSet; + } + + public int calculateGroupSimilarityScore(Group based, Group candidate) { + int score = 0; + String gameId = based.getGameId(); + String candidateGameId = candidate.getGameId(); + + + if (gameId.equals(candidateGameId)) { + score += 10; + } + Game basedGame = gameRepository.findById(gameId).get(); + Game candidateGame = gameRepository.findById(candidateGameId).get(); + + int gameSimScore = gameService.calculateSimilarityScore(basedGame, candidateGame); + score += gameSimScore; + + List baseTags = based.getTags(); + baseTags.retainAll(candidate.getTags()); + + for (String tagId : baseTags) { + Optional findTag = tagRepository.findByIdAndIsDeletedFalse(tagId); + if (findTag.isPresent()) { + score++; + } + } + return score; + } + } diff --git a/app/backend/src/main/java/com/app/gamereview/service/NotificationService.java b/app/backend/src/main/java/com/app/gamereview/service/NotificationService.java new file mode 100644 index 00000000..e6c83abd --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/service/NotificationService.java @@ -0,0 +1,66 @@ +package com.app.gamereview.service; + +import com.app.gamereview.dto.request.notification.CreateNotificationRequestDto; +import com.app.gamereview.dto.request.notification.GetNotificationsRequestDto; +import com.app.gamereview.model.Notification; +import com.app.gamereview.repository.*; +import org.modelmapper.ModelMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.ArrayList; +@Service +public class NotificationService { + private final NotificationRepository notificationRepository; + + private final MongoTemplate mongoTemplate; + private final ModelMapper modelMapper; + + @Autowired + public NotificationService(NotificationRepository notificationRepository, MongoTemplate mongoTemplate, + ModelMapper modelMapper) { + this.notificationRepository = notificationRepository; + this.mongoTemplate = mongoTemplate; + this.modelMapper = modelMapper; + } + + public List getNotificationList(String userId, GetNotificationsRequestDto filter) { + Query query = new Query(); + query.addCriteria(Criteria.where("user").is(userId)); + if(filter != null && filter.getIsRead() != null){ + query.addCriteria(Criteria.where("isRead").is(filter.getIsRead())); + } + query.with(Sort.by(Sort.Direction.DESC, "createdAt")); + List notificationList = mongoTemplate.find(query, Notification.class); + + // create a deep copy to return without isRead being changed + List copiedList = new ArrayList<>(); + for (Notification notification : notificationList) { + copiedList.add(new Notification(notification)); + } + + if(filter == null || filter.getIsRead() == null){ + for (Notification notification : notificationList) { + if(!notification.getIsRead()) { + notification.setIsRead(true); + notificationRepository.save(notification); + } + } + } + return copiedList; + } + + public Notification createNotification(CreateNotificationRequestDto request){ + Notification notificationToCreate = modelMapper.map(request, Notification.class); + notificationToCreate.setIsDeleted(false); + notificationToCreate.setCreatedAt(LocalDateTime.now()); + return notificationRepository.save(notificationToCreate); + } + +} diff --git a/app/backend/src/main/java/com/app/gamereview/service/PostService.java b/app/backend/src/main/java/com/app/gamereview/service/PostService.java index 5432939b..44d4f6ea 100644 --- a/app/backend/src/main/java/com/app/gamereview/service/PostService.java +++ b/app/backend/src/main/java/com/app/gamereview/service/PostService.java @@ -4,17 +4,19 @@ import java.util.*; import java.util.stream.Collectors; +import com.app.gamereview.dto.request.notification.CreateNotificationRequestDto; +import com.app.gamereview.dto.request.home.HomePagePostsFilterRequestDto; import com.app.gamereview.dto.response.comment.CommentReplyResponseDto; import com.app.gamereview.dto.response.comment.GetPostCommentsResponseDto; +import com.app.gamereview.dto.response.home.HomePagePostResponseDto; import com.app.gamereview.dto.response.post.GetPostDetailResponseDto; -import com.app.gamereview.enums.SortDirection; -import com.app.gamereview.enums.SortType; -import com.app.gamereview.enums.UserRole; -import com.app.gamereview.enums.VoteChoice; +import com.app.gamereview.enums.*; import com.app.gamereview.exception.BadRequestException; import com.app.gamereview.model.*; +import com.app.gamereview.model.Character; import com.app.gamereview.repository.*; import org.modelmapper.ModelMapper; +import org.modelmapper.PropertyMap; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; @@ -35,6 +37,10 @@ public class PostService { private final ForumRepository forumRepository; + private final GameRepository gameRepository; + + private final GroupRepository groupRepository; + private final UserRepository userRepository; private final ProfileRepository profileRepository; @@ -46,25 +52,42 @@ public class PostService { private final AchievementRepository achievementRepository; + private final CharacterRepository characterRepository; + private final MongoTemplate mongoTemplate; private final ModelMapper modelMapper; + private final NotificationService notificationService; + @Autowired public PostService(PostRepository postRepository, ForumRepository forumRepository, UserRepository userRepository, ProfileRepository profileRepository, TagRepository tagRepository, CommentRepository commentRepository, VoteRepository voteRepository, - AchievementRepository achievementRepository, MongoTemplate mongoTemplate, + AchievementRepository achievementRepository, GameRepository gameRepository, CharacterRepository characterRepository, MongoTemplate mongoTemplate, + NotificationService notificationService, GroupRepository groupRepository, ModelMapper modelMapper) { + this.postRepository = postRepository; this.forumRepository = forumRepository; + this.gameRepository = gameRepository; this.userRepository = userRepository; this.profileRepository = profileRepository; this.tagRepository = tagRepository; this.commentRepository = commentRepository; this.voteRepository = voteRepository; this.achievementRepository = achievementRepository; + this.characterRepository = characterRepository; this.mongoTemplate = mongoTemplate; this.modelMapper = modelMapper; + this.notificationService = notificationService; + this.groupRepository = groupRepository; + + modelMapper.addMappings(new PropertyMap() { + @Override + protected void configure() { + skip().setTags(null); // Exclude tags from mapping + } + }); } public List getPostList(GetPostListFilterRequestDto filter, String email) { @@ -123,6 +146,9 @@ private GetPostListResponseDto mapToGetPostListResponseDto(Post post, String log Optional postAchievementOptional = achievementRepository.findByIdAndIsDeletedFalse(post.getAchievement()); Achievement postAchievement = postAchievementOptional.orElse(null); + Optional postCharacterOptional = characterRepository.findByIdAndIsDeletedFalse(post.getCharacter()); + Character postCharacter = postCharacterOptional.orElse(null); + // Fetch tags individually for (String tagId : post.getTags()) { Optional tag = tagRepository.findById(tagId); @@ -130,7 +156,7 @@ private GetPostListResponseDto mapToGetPostListResponseDto(Post post, String log } return new GetPostListResponseDto(post.getId(), post.getTitle(), post.getPostContent(), - posterObject, userVoteChoice, post.getPostImage(), postAchievement, post.getLastEditedAt(), post.getCreatedAt(), isEdited, + posterObject, userVoteChoice, post.getPostImage(), postAchievement, postCharacter, post.getLastEditedAt(), post.getCreatedAt(), isEdited, tags, post.getInappropriate(), post.getOverallVote(), post.getVoteCount(), commentCount); } @@ -165,6 +191,10 @@ public GetPostDetailResponseDto getPostById(String id, String email) { postAchievement.ifPresent(postDto::setAchievement); + Optional postCharacter = characterRepository.findByIdAndIsDeletedFalse(post.get().getCharacter()); + + postCharacter.ifPresent(postDto::setCharacter); + List tags = new ArrayList<>(); // Fetch tags individually @@ -181,17 +211,21 @@ public GetPostDetailResponseDto getPostById(String id, String email) { return postDto; } - public List getCommentList(String postId, User user) { + public List getCommentList(String postId, String email) { Optional post = postRepository.findById(postId); + Optional loggedInUser = userRepository.findByEmailAndIsDeletedFalse(email); + String loggedInUserId = loggedInUser.map(User::getId).orElse(null); if (post.isEmpty()) { throw new ResourceNotFoundException("The post with the given id was not found"); } Optional forum = forumRepository.findById(post.get().getForum()); if (forum.isPresent()) { - List bannedUsers = forum.get().getBannedUsers(); - if (bannedUsers.contains(user.getId())) { - throw new ResourceNotFoundException("You cannot see the comments because you are banned from this forum."); + if(loggedInUserId != null){ + List bannedUsers = forum.get().getBannedUsers(); + if (bannedUsers.contains(loggedInUserId)) { + throw new ResourceNotFoundException("You cannot see the post because you are banned."); + } } } List comments = commentRepository.findByPost(postId); @@ -201,7 +235,7 @@ public List getCommentList(String postId, User user) // First, convert all comments to DTOs and identify top-level comments for (Comment comment : comments) { - GetPostCommentsResponseDto dto = convertToCommentDto(comment); + GetPostCommentsResponseDto dto = convertToCommentDto(comment, loggedInUserId); commentMap.put(comment.getId(), dto); if (comment.getParentComment() == null) { @@ -214,7 +248,7 @@ public List getCommentList(String postId, User user) if (comment.getParentComment() != null) { GetPostCommentsResponseDto parentDto = commentMap.get(comment.getParentComment()); if (parentDto != null) { - parentDto.getReplies().add(convertToReplyDto(comment)); + parentDto.getReplies().add(convertToReplyDto(comment, loggedInUserId)); } } } @@ -222,37 +256,45 @@ public List getCommentList(String postId, User user) return topLevelComments; } - private GetPostCommentsResponseDto convertToCommentDto(Comment comment) { + private GetPostCommentsResponseDto convertToCommentDto(Comment comment, String loggedInUserId) { Boolean isEdited = comment.getCreatedAt().isBefore(comment.getLastEditedAt()); String commenterId = comment.getCommenter(); Optional commenter = userRepository.findByIdAndIsDeletedFalse(commenterId); + Optional userVote = voteRepository.findByTypeIdAndVotedBy(comment.getId(), loggedInUserId); + VoteChoice userVoteChoice = userVote.map(Vote::getChoice).orElse(null); + User commenterObject = commenter.orElse(null); return new GetPostCommentsResponseDto(comment.getId(), comment.getCommentContent(), commenterObject, comment.getPost(), comment.getLastEditedAt(), comment.getCreatedAt(), isEdited, comment.getIsDeleted(), comment.getOverallVote(), - comment.getVoteCount(), new ArrayList<>()); + comment.getVoteCount(), new ArrayList<>(), userVoteChoice); } - private CommentReplyResponseDto convertToReplyDto(Comment comment) { + private CommentReplyResponseDto convertToReplyDto(Comment comment, String loggedInUserId) { Boolean isEdited = comment.getCreatedAt().isBefore(comment.getLastEditedAt()); String commenterId = comment.getCommenter(); Optional commenter = userRepository.findByIdAndIsDeletedFalse(commenterId); + Optional userVote = voteRepository.findByTypeIdAndVotedBy(comment.getId(), loggedInUserId); + VoteChoice userVoteChoice = userVote.map(Vote::getChoice).orElse(null); + User commenterObject = commenter.orElse(null); return new CommentReplyResponseDto(comment.getId(), comment.getCommentContent(), commenterObject, comment.getPost(), comment.getLastEditedAt(), comment.getCreatedAt(), isEdited, comment.getIsDeleted(), comment.getOverallVote(), - comment.getVoteCount()); + comment.getVoteCount(), userVoteChoice); } public Post createPost(CreatePostRequestDto request, User user) { - Optional forum = forumRepository.findById(request.getForum()); + Optional optionalForum = forumRepository.findById(request.getForum()); - if (forum.isEmpty()) { + if (optionalForum.isEmpty()) { throw new ResourceNotFoundException("Forum is not found."); } + Forum forum = optionalForum.get(); + if(request.getAchievement() != null){ // if achievement is assigned Optional achievementOptional = achievementRepository.findById(request.getAchievement()); @@ -261,6 +303,14 @@ public Post createPost(CreatePostRequestDto request, User user) { } } + if(request.getCharacter() != null){ // if character is assigned + Optional characterOptional = characterRepository.findById(request.getCharacter()); + + if (characterOptional.isEmpty()) { + throw new ResourceNotFoundException("Character is not found."); + } + } + if (request.getTags() != null) { for (String tagId : request.getTags()) { Optional tag = tagRepository.findById(tagId); @@ -286,6 +336,14 @@ public Post createPost(CreatePostRequestDto request, User user) { achievementRepository.findByIdAndIsDeletedFalse("88f359cc-8ca3-4286-bc13-1b44262ee9f4"); achievement.ifPresent(value -> profile.addAchievement(value.getId())); profile.setIsPostedYet(true); + CreateNotificationRequestDto createNotificationRequestDto= new CreateNotificationRequestDto(); + String message = NotificationMessage.FIRST_POST_ACHIEVEMENT.getMessageTemplate() + .replace("{user_name}", user.getUsername()) + .replace("{forum_name}", forum.getName()); + createNotificationRequestDto.setMessage(message); + createNotificationRequestDto.setParentType(NotificationParent.ACHIEVEMENT); + createNotificationRequestDto.setUser(user.getId()); + notificationService.createNotification(createNotificationRequestDto); } profile.setPostCount(profile.getPostCount() + 1); @@ -296,7 +354,34 @@ public Post createPost(CreatePostRequestDto request, User user) { postToCreate.setLastEditedAt(postToCreate.getCreatedAt()); postToCreate.setInappropriate(false); postToCreate.setLocked(false); - return postRepository.save(postToCreate); + Post savedPost = postRepository.save(postToCreate); + + if(forum.getType() == ForumType.GROUP){ + Group group = mongoTemplate.findById(forum.getParent(), Group.class); + if(group != null){ + if(group.getMembershipPolicy() == MembershipPolicy.PRIVATE) { + // send notification to group members + List members = group.getMembers(); + for(String member : members){ + if(!member.equals(user.getId())){ + Optional userOptional = userRepository.findById(member); + if(userOptional.isPresent()){ + User memberUser = userOptional.get(); + CreateNotificationRequestDto createNotificationRequestDto = new CreateNotificationRequestDto(); + String message = NotificationMessage.NEW_POST_IN_PRIVATE_GROUP.getMessageTemplate() + .replace("{user_name}", memberUser.getUsername()) + .replace("{group_title}", group.getTitle()); + createNotificationRequestDto.setMessage(message); + createNotificationRequestDto.setParentType(NotificationParent.POST); + createNotificationRequestDto.setParent(savedPost.getId()); + createNotificationRequestDto.setUser(member); + notificationService.createNotification(createNotificationRequestDto); } + } + } + } + } + } + return savedPost; } public Post editPost(String id, EditPostRequestDto request, User user) { @@ -367,4 +452,354 @@ public List getUserPostList(User user) { return mongoTemplate.find(query, Post.class); } + + public List getHomepagePosts(HomePagePostsFilterRequestDto filter, String email){ + if(email == null){ + return getHomePagePostsOfGuest(filter); + } + Optional findUser = userRepository.findByEmailAndIsDeletedFalse(email); + + if(findUser.isEmpty()){ + throw new ResourceNotFoundException("User not found"); + } + + return getHomePagePostsOfUser(filter, findUser.get()); + } + + public List getHomePagePostsOfGuest(HomePagePostsFilterRequestDto filter){ + Query query = new Query(); + query.addCriteria(Criteria.where("type").is(ForumType.GAME.name())); + List gameForums = mongoTemplate.find(query, Forum.class); + List postsToShow = new ArrayList<>(); + + Query allGamesQuery = new Query(); + allGamesQuery.addCriteria(Criteria.where("isDeleted").is(false)); + allGamesQuery.addCriteria(Criteria.where("isPromoted").is(true)); + List promotedGames = mongoTemplate.find(allGamesQuery, Game.class); + int promotedCount = 0; + if(promotedGames.size() >= 2){ + Collections.shuffle(promotedGames); + Game randomGame1 = promotedGames.get(0); + Game randomGame2 = promotedGames.get(1); + List game1Posts = postRepository.findByForumAndIsDeletedFalse(randomGame1.getForum()); + List game2Posts = postRepository.findByForumAndIsDeletedFalse(randomGame2.getForum()); + Collections.shuffle(game1Posts); + Collections.shuffle(game2Posts); + if(!game1Posts.isEmpty()){ + postsToShow.add(game1Posts.get(0)); + promotedCount++; + } + if(!game2Posts.isEmpty()){ + postsToShow.add(game2Posts.get(0)); + promotedCount++; + } + }else if(promotedGames.size() == 1){ + Game promotedGame = promotedGames.get(0); + List posts = postRepository.findByForumAndIsDeletedFalse(promotedGame.getForum()); + Collections.shuffle(posts); + if(!posts.isEmpty()){ + postsToShow.add(posts.get(0)); + postsToShow.add(posts.get(1)); + promotedCount= promotedCount+2; + } + } + + for(Forum forum : gameForums){ + postsToShow.addAll(postRepository.findByForumAndIsDeletedFalse(forum.getId())); + } + + if(filter.getSortBy().equals(SortType.CREATION_DATE.name())){ + if(filter.getSortDirection().equals(SortDirection.DESCENDING.name())){ + // descending + postsToShow.sort(Comparator.comparing(Post::getCreatedAt, Comparator.reverseOrder())); + } + else{ + // ascending + postsToShow.sort(Comparator.comparing(Post::getCreatedAt)); + } + } + else if(filter.getSortBy().equals(SortType.OVERALL_VOTE.name())){ + if(filter.getSortDirection().equals(SortDirection.DESCENDING.name())){ + // descending + postsToShow.sort(Comparator.comparing(Post::getOverallVote, Comparator.reverseOrder())); + } + else{ + // ascending + postsToShow.sort(Comparator.comparing(Post::getOverallVote)); + } + } + else if(filter.getSortBy().equals(SortType.VOTE_COUNT.name())){ + if(filter.getSortDirection().equals(SortDirection.DESCENDING.name())){ + // descending + postsToShow.sort(Comparator.comparing(Post::getVoteCount, Comparator.reverseOrder())); + } + else{ + // ascending + postsToShow.sort(Comparator.comparing(Post::getVoteCount)); + } + } + + List first20 = postsToShow.subList(0, Math.min(20, postsToShow.size())); + + List first20dto = new ArrayList<>(); + int index = 0; + for(Post post : first20){ + HomePagePostResponseDto dto = modelMapper.map(post,HomePagePostResponseDto.class); + + Optional findForum = forumRepository.findByIdAndIsDeletedFalse(post.getForum()); + + if(findForum.isEmpty()){ + continue; + } + + Forum forumOfPost = findForum.get(); + + dto.setType(forumOfPost.getType()); + dto.setTypeId(forumOfPost.getParent()); + + String typeName = null; + + if(forumOfPost.getType().equals(ForumType.GROUP)){ + Optional findGroup = groupRepository.findByIdAndIsDeletedFalse(forumOfPost.getParent()); + + if(findGroup.isEmpty()){ + throw new ResourceNotFoundException("Group not found"); + } + + typeName = findGroup.get().getTitle(); + } + + else if(forumOfPost.getType().equals(ForumType.GAME)){ + Optional findGame = gameRepository.findByIdAndIsDeletedFalse(forumOfPost.getParent()); + + if(findGame.isEmpty()){ + throw new ResourceNotFoundException("Game not found"); + } + + typeName = findGame.get().getGameName(); + } + + Optional findPoster = userRepository.findByIdAndIsDeletedFalse(post.getPoster()); + findPoster.ifPresent(dto::setPoster); + + Optional findAchievement = achievementRepository.findByIdAndIsDeletedFalse(post.getAchievement()); + findAchievement.ifPresent(dto::setAchievement); + + Optional findCharacter = characterRepository.findByIdAndIsDeletedFalse(post.getCharacter()); + findCharacter.ifPresent(dto::setCharacter); + + dto.setUserVote(null); + + dto.setTypeName(typeName); + + if(index<=promotedCount){ + dto.setIsPromoted(true); + }else { + dto.setIsPromoted(false); + } + index++; + first20dto.add(dto); + } + + return first20dto; + } + + public List getHomePagePostsOfUser(HomePagePostsFilterRequestDto filter, User user){ + Optional findProfile = profileRepository.findByUserIdAndIsDeletedFalse(user.getId()); + + if(findProfile.isEmpty()) { + throw new ResourceNotFoundException("User doesn't have any profile, unexpected error has occurred"); + } + + Profile profileOfUser = findProfile.get(); + List gameIds = profileOfUser.getGames(); + + List postsToShow = new ArrayList<>(); + + // get game posts (games that user follows) + for(String gameId : gameIds){ + Optional findGame = gameRepository.findByIdAndIsDeletedFalse(gameId); + + if(findGame.isEmpty()){ + continue; + } + + String forumId = findGame.get().getForum(); + + postsToShow.addAll(postRepository.findByForumAndIsDeletedFalse(forumId)); + } + + // get group posts (groups that user is a member of) + Query query = new Query(); + query.addCriteria(Criteria.where("members").in(user.getId())); + List groupsUserIsMemberOf = mongoTemplate.find(query, Group.class); + + for(Group group : groupsUserIsMemberOf){ + postsToShow.addAll(postRepository.findByForumAndIsDeletedFalse(group.getForumId())); + } + + if(filter.getSortBy().equals(SortType.CREATION_DATE.name())){ + if(filter.getSortDirection().equals(SortDirection.DESCENDING.name())){ + // descending + postsToShow.sort(Comparator.comparing(Post::getCreatedAt, Comparator.reverseOrder())); + } + else{ + // ascending + postsToShow.sort(Comparator.comparing(Post::getCreatedAt)); + } + } + else if(filter.getSortBy().equals(SortType.OVERALL_VOTE.name())){ + if(filter.getSortDirection().equals(SortDirection.DESCENDING.name())){ + // descending + postsToShow.sort(Comparator.comparing(Post::getOverallVote, Comparator.reverseOrder())); + } + else{ + // ascending + postsToShow.sort(Comparator.comparing(Post::getOverallVote)); + } + } + else if(filter.getSortBy().equals(SortType.VOTE_COUNT.name())){ + if(filter.getSortDirection().equals(SortDirection.DESCENDING.name())){ + // descending + postsToShow.sort(Comparator.comparing(Post::getVoteCount, Comparator.reverseOrder())); + } + else{ + // ascending + postsToShow.sort(Comparator.comparing(Post::getVoteCount)); + } + } + + if(postsToShow.size() < 20){ // complete to 20 + Query postQuery = new Query(); + postQuery.addCriteria(Criteria.where("isDeleted").is(false)); + postQuery.with(Sort.by(Sort.Order.desc("overallVote"))); + List allPosts = mongoTemplate.find(postQuery,Post.class); + int toAdd = 20 - postsToShow.size(); + for(Post post : allPosts){ + if(!postsToShow.contains(post)){ + postsToShow.add(post); + toAdd--; + } + if(toAdd == 0) + break; + } + } + + List first20dto = new ArrayList<>(); + + Query allGamesQuery = new Query(); + allGamesQuery.addCriteria(Criteria.where("isDeleted").is(false)); + allGamesQuery.addCriteria(Criteria.where("isPromoted").is(true)); + List promotedGames = mongoTemplate.find(allGamesQuery, Game.class); + int promotedCount = 0; + if(promotedGames.size() >= 2){ + Collections.shuffle(promotedGames); + Game randomGame1 = promotedGames.get(0); + Game randomGame2 = promotedGames.get(1); + List game1Posts = postRepository.findByForumAndIsDeletedFalse(randomGame1.getForum()); + List game2Posts = postRepository.findByForumAndIsDeletedFalse(randomGame2.getForum()); + Collections.shuffle(game1Posts); + Collections.shuffle(game2Posts); + if(!game1Posts.isEmpty()){ + postsToShow.add(0,game1Posts.get(0)); + promotedCount++; + } + if(!game2Posts.isEmpty()){ + postsToShow.add(0,game2Posts.get(0)); + promotedCount++; + } + }else if(promotedGames.size() == 1){ + Game promotedGame = promotedGames.get(0); + List posts = postRepository.findByForumAndIsDeletedFalse(promotedGame.getForum()); + Collections.shuffle(posts); + if(!posts.isEmpty()){ + postsToShow.add(0,posts.get(0)); + postsToShow.add(0,posts.get(1)); + promotedCount = promotedCount +2; + + } + } + List first20 = postsToShow.subList(0, Math.min(20, postsToShow.size())); + int index = 0; + for(Post post : first20){ + HomePagePostResponseDto dto = modelMapper.map(post,HomePagePostResponseDto.class); + dto.setTags(populatedTags(post.getTags())); + + Optional findForum = forumRepository.findByIdAndIsDeletedFalse(post.getForum()); + + if(findForum.isEmpty()){ + continue; + } + + Forum forumOfPost = findForum.get(); + + dto.setType(forumOfPost.getType()); + dto.setTypeId(forumOfPost.getParent()); + + String typeName = null; + + if(forumOfPost.getType().equals(ForumType.GROUP)){ + Optional findGroup = groupRepository.findByIdAndIsDeletedFalse(forumOfPost.getParent()); + + if(findGroup.isEmpty()){ + throw new ResourceNotFoundException("Group not found"); + } + + typeName = findGroup.get().getTitle(); + } + + else if(forumOfPost.getType().equals(ForumType.GAME)){ + Optional findGame = gameRepository.findByIdAndIsDeletedFalse(forumOfPost.getParent()); + + if(findGame.isEmpty()){ + throw new ResourceNotFoundException("Game not found"); + } + + typeName = findGame.get().getGameName(); + } + + Optional findPoster = userRepository.findByIdAndIsDeletedFalse(post.getPoster()); + findPoster.ifPresent(dto::setPoster); + + Optional findAchievement = achievementRepository.findByIdAndIsDeletedFalse(post.getAchievement()); + findAchievement.ifPresent(dto::setAchievement); + + Optional findCharacter = characterRepository.findByIdAndIsDeletedFalse(post.getCharacter()); + findCharacter.ifPresent(dto::setCharacter); + + dto.setUserVote(getUserVote(post.getId(), user.getId())); + + dto.setTypeName(typeName); + if(index<=promotedCount){ + dto.setIsPromoted(true); + }else { + dto.setIsPromoted(false); + } + index++; + first20dto.add(dto); + } + + return first20dto; + } + + public VoteChoice getUserVote(String postId, String userId){ + Optional findPost = postRepository.findByIdAndIsDeletedFalse(postId); + + if(findPost.isEmpty()) return null; + + Optional userVote = voteRepository.findByTypeIdAndVotedBy(postId,userId); + + return userVote.map(Vote::getChoice).orElse(null); + } + + public List populatedTags(List tagIds){ + List res = new ArrayList<>(); + + for(String tagId : tagIds){ + Optional findTag = tagRepository.findByIdAndIsDeletedFalse(tagId); + findTag.ifPresent(res::add); + } + + return res; + } } diff --git a/app/backend/src/main/java/com/app/gamereview/service/ReviewService.java b/app/backend/src/main/java/com/app/gamereview/service/ReviewService.java index c016f951..f2f11d17 100644 --- a/app/backend/src/main/java/com/app/gamereview/service/ReviewService.java +++ b/app/backend/src/main/java/com/app/gamereview/service/ReviewService.java @@ -1,12 +1,11 @@ package com.app.gamereview.service; +import com.app.gamereview.dto.request.notification.CreateNotificationRequestDto; import com.app.gamereview.dto.request.review.CreateReviewRequestDto; import com.app.gamereview.dto.request.review.GetAllReviewsFilterRequestDto; import com.app.gamereview.dto.request.review.UpdateReviewRequestDto; import com.app.gamereview.dto.response.review.GetAllReviewsResponseDto; -import com.app.gamereview.enums.SortDirection; -import com.app.gamereview.enums.SortType; -import com.app.gamereview.enums.UserRole; +import com.app.gamereview.enums.*; import com.app.gamereview.exception.BadRequestException; import com.app.gamereview.exception.ResourceNotFoundException; import com.app.gamereview.model.*; @@ -41,6 +40,7 @@ public class ReviewService { private final MongoTemplate mongoTemplate; private final ModelMapper modelMapper; + private final NotificationService notificationService; @Autowired public ReviewService( @@ -51,7 +51,8 @@ public ReviewService( UserRepository userRepository, ProfileRepository profileRepository, MongoTemplate mongoTemplate, - ModelMapper modelMapper + ModelMapper modelMapper, + NotificationService notificationService ) { this.reviewRepository = reviewRepository; this.gameRepository = gameRepository; @@ -61,6 +62,7 @@ public ReviewService( this.profileRepository = profileRepository; this.mongoTemplate = mongoTemplate; this.modelMapper = modelMapper; + this.notificationService = notificationService; modelMapper.addMappings(new PropertyMap() { @Override @@ -150,21 +152,28 @@ public Review addReview(CreateReviewRequestDto requestDto, User user){ throw new ResourceNotFoundException("Profile of the user does not exist"); } + // add game rating + Game reviewedGame = gameRepository.findById(reviewToCreate.getGameId()).get(); + reviewedGame.addRating(reviewToCreate.getRating()); + gameRepository.save(reviewedGame); + if(!profile.getIsReviewedYet()){ // first review of the user Optional achievement = achievementRepository.findByIdAndIsDeletedFalse("405564b0-fbc0-4864-853c-c6e8e4cd2acd"); achievement.ifPresent(value -> profile.addAchievement(value.getId())); profile.setIsReviewedYet(true); + CreateNotificationRequestDto createNotificationRequestDto= new CreateNotificationRequestDto(); + String message = NotificationMessage.FIRST_REVIEW_ACHIEVEMENT.getMessageTemplate() + .replace("{user_name}", user.getUsername()) + .replace("{game_name}", reviewedGame.getGameName()); + createNotificationRequestDto.setParentType(NotificationParent.ACHIEVEMENT); + createNotificationRequestDto.setMessage(message); + createNotificationRequestDto.setUser(user.getId()); + notificationService.createNotification(createNotificationRequestDto); } - profile.setReviewCount(profile.getReviewCount() + 1); profileRepository.save(profile); - // add game rating - Game reviewedGame = gameRepository.findById(reviewToCreate.getGameId()).get(); - reviewedGame.addRating(reviewToCreate.getRating()); - gameRepository.save(reviewedGame); - reviewRepository.save(reviewToCreate); return reviewToCreate; } diff --git a/app/backend/src/main/java/com/app/gamereview/service/VoteService.java b/app/backend/src/main/java/com/app/gamereview/service/VoteService.java index 9e4943f1..d163cb14 100644 --- a/app/backend/src/main/java/com/app/gamereview/service/VoteService.java +++ b/app/backend/src/main/java/com/app/gamereview/service/VoteService.java @@ -1,7 +1,10 @@ package com.app.gamereview.service; +import com.app.gamereview.dto.request.notification.CreateNotificationRequestDto; import com.app.gamereview.dto.request.vote.CreateVoteRequestDto; import com.app.gamereview.dto.request.vote.GetAllVotesFilterRequestDto; +import com.app.gamereview.enums.NotificationMessage; +import com.app.gamereview.enums.NotificationParent; import com.app.gamereview.enums.VoteChoice; import com.app.gamereview.enums.VoteType; import com.app.gamereview.exception.ResourceNotFoundException; @@ -15,6 +18,8 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -33,6 +38,10 @@ public class VoteService { private final ModelMapper modelMapper; private final PostRepository postRepository; private final CommentRepository commentRepository; + private final NotificationRepository notificationRepository; + private final UserRepository userRepository; + private final NotificationService notificationService; + @Autowired public VoteService( @@ -40,18 +49,25 @@ public VoteService( ReviewRepository reviewRepository, ProfileRepository profileRepository, AchievementRepository achievementRepository, + NotificationRepository notificationRepository, + UserRepository userRepository, MongoTemplate mongoTemplate, ModelMapper modelMapper, PostRepository postRepository, - CommentRepository commentRepository) { + CommentRepository commentRepository, + NotificationService notificationService) { this.voteRepository = voteRepository; this.reviewRepository = reviewRepository; this.profileRepository = profileRepository; this.achievementRepository = achievementRepository; + this.notificationRepository = notificationRepository; this.mongoTemplate = mongoTemplate; this.modelMapper = modelMapper; this.postRepository = postRepository; this.commentRepository = commentRepository; + this.userRepository = userRepository; + this.notificationService = notificationService; + modelMapper.addMappings(new PropertyMap() { @Override @@ -110,6 +126,13 @@ public Vote addVote(CreateVoteRequestDto requestDto, User user) { achievementRepository.findByIdAndIsDeletedFalse("eb558639-32ca-413f-b842-c3788287dd05"); achievement.ifPresent(value -> profile.addAchievement(value.getId())); profile.setIsVotedYet(true); + CreateNotificationRequestDto createNotificationRequestDto= new CreateNotificationRequestDto(); + String message = NotificationMessage.FIRST_VOTE_ACHIEVEMENT.getMessageTemplate() + .replace("{user_name}", user.getUsername()); + createNotificationRequestDto.setMessage(message); + createNotificationRequestDto.setParentType(NotificationParent.ACHIEVEMENT); + createNotificationRequestDto.setUser(user.getId()); + notificationService.createNotification(createNotificationRequestDto); } @@ -235,6 +258,42 @@ else if (!prevVote.isEmpty() && Post post = optionalPost.get(); post.addVote(choice); postRepository.save(post); + + + if(post.getVoteCount() == 1 && notificationRepository.findByParentAndUser(post.getId(), user.getId()).isEmpty()) { + CreateNotificationRequestDto createNotificationRequestDto= new CreateNotificationRequestDto(); + Optional optionalUser = userRepository.findById(post.getPoster()); + if(optionalUser.isPresent()) { + User poster = optionalUser.get(); + String message = NotificationMessage.FIRST_VOTE_OF_THE_POST.getMessageTemplate() + .replace("{user_name}", poster.getUsername()) + .replace("{post_title}", post.getTitle()); + createNotificationRequestDto.setMessage(message); + createNotificationRequestDto.setParentType(NotificationParent.POST); + createNotificationRequestDto.setParent(post.getId()); + createNotificationRequestDto.setUser(post.getPoster()); + notificationService.createNotification(createNotificationRequestDto); + } + } + + List notificationVoteCounts = new ArrayList<>(Arrays.asList(5, 10, 15)); + if(notificationVoteCounts.contains(post.getOverallVote())) { + CreateNotificationRequestDto createNotificationRequestDto= new CreateNotificationRequestDto(); + Optional optionalUser = userRepository.findById(post.getPoster()); + if(optionalUser.isPresent()) { + User poster = optionalUser.get(); + String message = NotificationMessage.NTH_VOTE_OF_THE_POST.getMessageTemplate() + .replace("{user_name}", poster.getUsername()) + .replace("{post_title}", post.getTitle()) + .replace("{overall_vote}", String.valueOf(post.getOverallVote())); + createNotificationRequestDto.setMessage(message); + createNotificationRequestDto.setParentType(NotificationParent.POST); + createNotificationRequestDto.setParent(post.getId()); + createNotificationRequestDto.setUser(post.getPoster()); + notificationService.createNotification(createNotificationRequestDto); + } + } + return voteRepository.save(voteToCreate); } else if (requestDto.getVoteType().equals(VoteType.COMMENT.name())) { // delete previous vote diff --git a/app/backend/src/main/java/com/app/gamereview/util/validation/annotation/ValidApplicationReviewResult.java b/app/backend/src/main/java/com/app/gamereview/util/validation/annotation/ValidApplicationReviewResult.java new file mode 100644 index 00000000..0706899b --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/util/validation/annotation/ValidApplicationReviewResult.java @@ -0,0 +1,24 @@ +package com.app.gamereview.util.validation.annotation; + +import com.app.gamereview.enums.GroupApplicationReviewResult; +import com.app.gamereview.util.validation.validator.ApplicationReviewResultValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ApplicationReviewResultValidator.class) +public @interface ValidApplicationReviewResult { + GroupApplicationReviewResult[] allowedValues(); + + String message() default "Invalid Review Result, allowed values are: {allowedValues}"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/app/backend/src/main/java/com/app/gamereview/util/validation/validator/ApplicationReviewResultValidator.java b/app/backend/src/main/java/com/app/gamereview/util/validation/validator/ApplicationReviewResultValidator.java new file mode 100644 index 00000000..51a95b84 --- /dev/null +++ b/app/backend/src/main/java/com/app/gamereview/util/validation/validator/ApplicationReviewResultValidator.java @@ -0,0 +1,24 @@ +package com.app.gamereview.util.validation.validator; + +import com.app.gamereview.enums.GroupApplicationReviewResult; +import com.app.gamereview.util.validation.annotation.ValidApplicationReviewResult; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class ApplicationReviewResultValidator implements ConstraintValidator { + + @Override + public boolean isValid(String providedResult, ConstraintValidatorContext context) { + if (providedResult == null || providedResult.isEmpty()) { + return false; + } + + for (GroupApplicationReviewResult reviewResult : GroupApplicationReviewResult.values()) { + if (reviewResult.name().equals(providedResult)) { + return true; + } + } + + return false; + } +} diff --git a/app/backend/src/test/java/com/app/gamereview/service/AchievementServiceTest.java b/app/backend/src/test/java/com/app/gamereview/service/AchievementServiceTest.java new file mode 100644 index 00000000..ac631f00 --- /dev/null +++ b/app/backend/src/test/java/com/app/gamereview/service/AchievementServiceTest.java @@ -0,0 +1,206 @@ +package com.app.gamereview.service; + +import com.app.gamereview.dto.request.achievement.CreateAchievementRequestDto; +import com.app.gamereview.dto.request.achievement.GrantAchievementRequestDto; +import com.app.gamereview.dto.request.achievement.UpdateAchievementRequestDto; +import com.app.gamereview.enums.AchievementType; +import com.app.gamereview.exception.ResourceNotFoundException; +import com.app.gamereview.model.Achievement; +import com.app.gamereview.model.Game; +import com.app.gamereview.model.Profile; +import com.app.gamereview.repository.AchievementRepository; +import com.app.gamereview.repository.GameRepository; +import com.app.gamereview.repository.ProfileRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.modelmapper.ModelMapper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +public class AchievementServiceTest { + + @Mock + private AchievementRepository achievementRepository; + + @Mock + private GameRepository gameRepository; + + @Mock + private ProfileRepository profileRepository; + + @Mock + private ModelMapper modelMapper; + + @InjectMocks + private AchievementService achievementService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testCreateAchievement_GameAchievement_Success() { + // Arrange + CreateAchievementRequestDto requestDto = new CreateAchievementRequestDto(); + requestDto.setType(AchievementType.GAME.toString()); + requestDto.setGame("gameId"); + Achievement achievement = new Achievement(); + achievement.setGame("gameId"); + achievement.setType(AchievementType.valueOf(requestDto.getType())); + + when(gameRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(new Game())); + when(achievementRepository.findByTitleAndIsDeletedFalse(anyString())).thenReturn(new ArrayList<>()); + when(modelMapper.map(any(CreateAchievementRequestDto.class), eq(Achievement.class))).thenReturn(achievement); + when(achievementRepository.save(any(Achievement.class))).thenReturn(achievement); + // Act + Achievement result = achievementService.createAchievement(requestDto); + + // Assert + Assertions.assertNotNull(result); + verify(achievementRepository, times(1)).save(any(Achievement.class)); + } + + @Test + void testCreateAchievement_MetaAchievement_Success() { + // Arrange + CreateAchievementRequestDto requestDto = new CreateAchievementRequestDto(); + requestDto.setType(AchievementType.META.toString()); + Achievement achievement = new Achievement(); + achievement.setType(AchievementType.valueOf(requestDto.getType())); + + when(achievementRepository.findByTitleAndIsDeletedFalse(anyString())).thenReturn(new ArrayList<>()); + when(modelMapper.map(any(CreateAchievementRequestDto.class), eq(Achievement.class))).thenReturn(achievement); + when(achievementRepository.save(any(Achievement.class))).thenReturn(achievement); + + // Act + Achievement result = achievementService.createAchievement(requestDto); + + // Assert + Assertions.assertNotNull(result); + verify(achievementRepository, times(1)).save(any(Achievement.class)); + } + + + @Test + void testUpdateAchievement_Success() { + // Arrange + String achievementId = "achievementId"; + UpdateAchievementRequestDto requestDto = new UpdateAchievementRequestDto(); + requestDto.setTitle("New Title"); + Achievement existingAchievement = new Achievement(); + existingAchievement.setId(achievementId); + when(achievementRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(existingAchievement)); + when(achievementRepository.findByTitleAndIsDeletedFalse(anyString())).thenReturn(Collections.emptyList()); + + // Act + Achievement result = achievementService.updateAchievement(achievementId, requestDto); + + // Assert + Assertions.assertNotNull(result); + Assertions.assertEquals(requestDto.getTitle(), result.getTitle()); + verify(achievementRepository, times(1)).save(any(Achievement.class)); + } + + @Test + void testDeleteAchievementById_Success() { + // Arrange + String achievementId = "achievementId"; + Achievement existingAchievement = new Achievement(); + existingAchievement.setId(achievementId); + when(achievementRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(existingAchievement)); + + // Act + Achievement result = achievementService.deleteAchievement(achievementId); + + // Assert + Assertions.assertNotNull(result); + Assertions.assertTrue(result.getIsDeleted()); + verify(achievementRepository, times(1)).save(any(Achievement.class)); + } + + @Test + void testDeleteAchievement_NotFoundById() { + // Arrange + String achievementId = "nonExistentAchievementId"; + + when(achievementRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.empty()); + + // Act and Assert + Assertions.assertThrows(ResourceNotFoundException.class, + () -> achievementService.deleteAchievement(achievementId), + "Should throw ResourceNotFoundException when achievement is not found by ID"); + + // Verify + verify(achievementRepository, times(1)).findByIdAndIsDeletedFalse(anyString()); + } + + @Test + void testDeleteAchievementByNameAndGame_Success() { + // Arrange + String achievementName = "achievementName"; + String gameName = "gameName"; + Achievement existingAchievement = new Achievement(); + Game existingGame = new Game(); + + existingAchievement.setTitle(achievementName); + existingGame.setGameName(gameName); + existingAchievement.setGame(existingGame.getId()); + + when(achievementRepository.findByTitleAndIsDeletedFalse(anyString())).thenReturn(Collections.singletonList(existingAchievement)); + when(gameRepository.findByGameNameAndIsDeletedFalse(anyString())).thenReturn(Optional.of(existingGame)); + when(gameRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(existingGame)); + + // Act + Achievement result = achievementService.deleteAchievement(achievementName, gameName); + + // Assert + Assertions.assertNotNull(result); + Assertions.assertTrue(result.getIsDeleted()); + verify(achievementRepository, times(1)).save(any(Achievement.class)); + } + + @Test + void testGrantAchievement_Success() { + // Arrange + String achievementId = "achievementId"; + String userId = "userId"; + GrantAchievementRequestDto requestDto = new GrantAchievementRequestDto(); + requestDto.setAchievementId(achievementId); + requestDto.setUserId(userId); + when(achievementRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(new Achievement())); + when(profileRepository.findByUserIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(new Profile())); + + // Act + List result = achievementService.grantAchievement(requestDto); + + // Assert + Assertions.assertNotNull(result); + verify(profileRepository, times(1)).save(any(Profile.class)); + } + + @Test + void testGetGameAchievements_Success() { + // Arrange + String gameId = "gameId"; + when(gameRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(new Game())); + when(achievementRepository.findByGameAndIsDeletedFalse(anyString())).thenReturn(Collections.emptyList()); + + // Act + List result = achievementService.getGameAchievements(gameId); + + // Assert + Assertions.assertNotNull(result); + verify(achievementRepository, times(1)).findByGameAndIsDeletedFalse(anyString()); + } +} diff --git a/app/backend/src/test/java/com/app/gamereview/service/CharacterServiceTest.java b/app/backend/src/test/java/com/app/gamereview/service/CharacterServiceTest.java new file mode 100644 index 00000000..e021cb2b --- /dev/null +++ b/app/backend/src/test/java/com/app/gamereview/service/CharacterServiceTest.java @@ -0,0 +1,344 @@ +package com.app.gamereview.service; + +import com.app.gamereview.dto.request.character.CreateCharacterRequestDto; +import com.app.gamereview.dto.request.character.UpdateCharacterRequestDto; +import com.app.gamereview.exception.BadRequestException; +import com.app.gamereview.exception.ResourceNotFoundException; +import com.app.gamereview.model.Character; +import com.app.gamereview.model.Game; +import com.app.gamereview.repository.CharacterRepository; +import com.app.gamereview.repository.GameRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.modelmapper.ModelMapper; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CharacterServiceTest { + + @Mock + private CharacterRepository characterRepository; + + @Mock + private GameRepository gameRepository; + + @Mock + private ModelMapper modelMapper; + + @InjectMocks + private CharacterService characterService; + + @BeforeEach + void setUp() { + // Set up any common mocking behavior here + } + + @Test + void testCreateCharacterWithValidData() { + // Arrange + CreateCharacterRequestDto requestDto = new CreateCharacterRequestDto(); + requestDto.setGames(Collections.singletonList("validGameId")); + Character character = new Character(); + when(modelMapper.map(requestDto, Character.class)).thenReturn(character); + when(gameRepository.findByIdAndIsDeletedFalse("validGameId")).thenReturn(Optional.of(new Game())); + when(characterRepository.save(any(Character.class))).thenReturn(character); + + // Act + Character createdCharacter = characterService.createCharacter(requestDto); + + // Assert + assertNotNull(createdCharacter, "Character should not be null"); + } + + @Test + void testCreateCharacterWithEmptyGames() { + // Arrange + CreateCharacterRequestDto requestDto = new CreateCharacterRequestDto(); + requestDto.setGames(Collections.emptyList()); + + // Act & Assert + assertThrows(BadRequestException.class, + () -> characterService.createCharacter(requestDto), + "Expected BadRequestException to be thrown when games list is empty" + ); + } + + @Test + void testCreateCharacterWithNonExistentGame() { + // Arrange + CreateCharacterRequestDto requestDto = new CreateCharacterRequestDto(); + requestDto.setGames(Collections.singletonList("nonExistentGameId")); + when(gameRepository.findByIdAndIsDeletedFalse("nonExistentGameId")).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, + () -> characterService.createCharacter(requestDto), + "Expected ResourceNotFoundException to be thrown for a non-existent game" + ); + } + + @Test + void testCreateCharacterWithNullRequest() { + // Act & Assert + assertThrows(NullPointerException.class, + () -> characterService.createCharacter(null), + "Expected NullPointerException to be thrown when request is null" + ); + } + + @Test + void testUpdateCharacterWithValidData() { + // Arrange + UpdateCharacterRequestDto requestDto = new UpdateCharacterRequestDto(); + requestDto.setName("UpdatedName"); + requestDto.setIcon("UpdatedIcon"); + requestDto.setDescription("UpdatedDescription"); + requestDto.setGames(Collections.singletonList("validGameId")); + requestDto.setType("UpdatedType"); + requestDto.setGender("UpdatedGender"); + requestDto.setRace("UpdatedRace"); + requestDto.setStatus("UpdatedStatus"); + requestDto.setOccupation("UpdatedOccupation"); + requestDto.setBirthDate("UpdatedBirthDate"); + requestDto.setVoiceActor("UpdatedVoiceActor"); + requestDto.setHeight("UpdatedHeight"); + requestDto.setAge("UpdatedAge"); + requestDto.setCustomFields(Collections.singletonMap("key", "value")); + + Character existingCharacter = new Character(); + when(characterRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(existingCharacter)); + when(gameRepository.findByIdAndIsDeletedFalse("validGameId")).thenReturn(Optional.of(new Game())); + when(characterRepository.save(any(Character.class))).thenReturn(existingCharacter); + + // Act + Character updatedCharacter = characterService.updateCharacter("existingId", requestDto); + + // Assert + assertNotNull(updatedCharacter, "Character should be updated and not null"); + assertEquals("UpdatedName", updatedCharacter.getName()); + } + + @Test + void testUpdateCharacterWithValidBlankData() { + // Arrange + UpdateCharacterRequestDto requestDto = new UpdateCharacterRequestDto(); + requestDto.setName("UpdatedName"); + requestDto.setIcon("UpdatedIcon"); + requestDto.setDescription("description"); + requestDto.setGames(Collections.singletonList("validGameId")); + requestDto.setType(""); + requestDto.setGender(""); + requestDto.setRace(""); + requestDto.setStatus(""); + requestDto.setOccupation(""); + requestDto.setBirthDate(""); + requestDto.setVoiceActor(""); + requestDto.setHeight(""); + requestDto.setAge(""); + requestDto.setCustomFields(Collections.singletonMap("key", "value")); + + Character existingCharacter = new Character(); + when(characterRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(existingCharacter)); + when(gameRepository.findByIdAndIsDeletedFalse("validGameId")).thenReturn(Optional.of(new Game())); + when(characterRepository.save(any(Character.class))).thenReturn(existingCharacter); + + // Act + Character updatedCharacter = characterService.updateCharacter("existingId", requestDto); + + // Assert + assertNotNull(updatedCharacter, "Character should be updated and not null"); + assertEquals("UpdatedName", updatedCharacter.getName()); + } + + @Test + void testUpdateCharacterNonExistentCharacter() { + // Arrange + when(characterRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, + () -> characterService.updateCharacter("nonExistentId", new UpdateCharacterRequestDto()), + "Expected ResourceNotFoundException for non-existent character ID" + ); + } + + @Test + void testUpdateCharacterWithBlankName() { + // Arrange + UpdateCharacterRequestDto requestDto = new UpdateCharacterRequestDto(); + requestDto.setName(""); + + Character existingCharacter = new Character(); + when(characterRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(existingCharacter)); + + // Act & Assert + assertThrows(BadRequestException.class, + () -> characterService.updateCharacter("existingId", requestDto), + "Expected BadRequestException when the name is blank" + ); + } + + @Test + void testUpdateCharacterWithBlankIcon() { + // Arrange + UpdateCharacterRequestDto requestDto = new UpdateCharacterRequestDto(); + requestDto.setIcon(""); + + Character existingCharacter = new Character(); + when(characterRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(existingCharacter)); + + // Act & Assert + assertThrows(BadRequestException.class, + () -> characterService.updateCharacter("existingId", requestDto), + "Expected BadRequestException when the icon is blank" + ); + } + + @Test + void testUpdateCharacterWithBlankDescription() { + // Arrange + UpdateCharacterRequestDto requestDto = new UpdateCharacterRequestDto(); + requestDto.setDescription(""); + + Character existingCharacter = new Character(); + when(characterRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(existingCharacter)); + + // Act & Assert + assertThrows(BadRequestException.class, + () -> characterService.updateCharacter("existingId", requestDto), + "Expected BadRequestException when the description is blank" + ); + } + + @Test + void testUpdateCharacterWithEmptyGamesList() { + // Arrange + UpdateCharacterRequestDto requestDto = new UpdateCharacterRequestDto(); + requestDto.setGames(Collections.emptyList()); + + Character existingCharacter = new Character(); + when(characterRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(existingCharacter)); + + // Act & Assert + assertThrows(BadRequestException.class, + () -> characterService.updateCharacter("existingId", requestDto), + "Expected BadRequestException when the games list is empty" + ); + } + + @Test + void testUpdateCharacterWithNonExistentGame() { + // Arrange + UpdateCharacterRequestDto requestDto = new UpdateCharacterRequestDto(); + requestDto.setGames(Collections.singletonList("nonExistentGameId")); + + Character existingCharacter = new Character(); + when(characterRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(existingCharacter)); + when(gameRepository.findByIdAndIsDeletedFalse("nonExistentGameId")).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, + () -> characterService.updateCharacter("existingId", requestDto), + "Expected ResourceNotFoundException for non-existent game in the games list" + ); + } + + @Test + void testUpdateCharacterWithNullRequest() { + // Arrange + Character existingCharacter = new Character(); + when(characterRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(existingCharacter)); + + // Act & Assert + assertThrows(NullPointerException.class, + () -> characterService.updateCharacter("existingId", null), + "Expected NullPointerException when request is null" + ); + } + + @Test + void testDeleteCharacterWithValidId() { + // Arrange + String validId = "validId"; + Character existingCharacter = new Character(); + when(characterRepository.findByIdAndIsDeletedFalse(validId)).thenReturn(Optional.of(existingCharacter)); + + // Act + Character deletedCharacter = characterService.deleteCharacter(validId); + + // Assert + assertTrue(deletedCharacter.getIsDeleted(), "Character should be marked as deleted"); + verify(characterRepository).save(deletedCharacter); + } + + @Test + void testDeleteNonExistentCharacter() { + // Arrange + when(characterRepository.findByIdAndIsDeletedFalse("nonExistentId")).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, + () -> characterService.deleteCharacter("nonExistentId"), + "Expected ResourceNotFoundException for a non-existent character ID" + ); + } + + @Test + void testDeleteCharacterAlreadyDeleted() { + // Arrange + String validId = "validId"; + Character alreadyDeletedCharacter = new Character(); + alreadyDeletedCharacter.setIsDeleted(true); + // create empty optional character object + Optional characterOptional = Optional.empty(); + when(characterRepository.findByIdAndIsDeletedFalse(validId)).thenReturn(characterOptional); + + // Act + assertThrows(ResourceNotFoundException.class, + () -> characterService.deleteCharacter(validId), + "Expected ResourceNotFoundException for a non-existent character ID" + ); + + // Assert + assertTrue(alreadyDeletedCharacter.getIsDeleted(), "Character should remain deleted"); + verify(characterRepository, never()).save(alreadyDeletedCharacter); + } + + @Test + void testGetGameCharactersWithValidGameId() { + // Arrange + String validGameId = "validGameId"; + when(gameRepository.findByIdAndIsDeletedFalse(validGameId)).thenReturn(Optional.of(new Game())); + when(characterRepository.findByGamesContains(validGameId)).thenReturn(Collections.singletonList(new Character())); + + // Act + List characters = characterService.getGameCharacters(validGameId); + + // Assert + assertFalse(characters.isEmpty(), "Character list should not be empty"); + } + + @Test + void testGetGameCharactersWithNonExistentGameId() { + // Arrange + when(gameRepository.findByIdAndIsDeletedFalse("nonExistentGameId")).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, + () -> characterService.getGameCharacters("nonExistentGameId"), + "Expected ResourceNotFoundException for a non-existent game ID" + ); + } +} \ No newline at end of file diff --git a/app/backend/src/test/java/com/app/gamereview/service/CommentServiceTests.java b/app/backend/src/test/java/com/app/gamereview/service/CommentServiceTests.java new file mode 100644 index 00000000..e934651e --- /dev/null +++ b/app/backend/src/test/java/com/app/gamereview/service/CommentServiceTests.java @@ -0,0 +1,174 @@ +package com.app.gamereview.service; + +import com.app.gamereview.dto.request.comment.CreateCommentRequestDto; +import com.app.gamereview.dto.request.comment.EditCommentRequestDto; +import com.app.gamereview.dto.request.comment.ReplyCommentRequestDto; +import com.app.gamereview.exception.ResourceNotFoundException; +import com.app.gamereview.model.Comment; +import com.app.gamereview.model.Post; +import com.app.gamereview.model.Profile; +import com.app.gamereview.model.User; +import com.app.gamereview.repository.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.modelmapper.ModelMapper; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CommentServiceTests { + + @Mock + private PostRepository postRepository; + @Mock + private AchievementRepository achievementRepository; + @Mock + private ProfileRepository profileRepository; + @Mock + private CommentRepository commentRepository; + @Mock + private UserRepository userRepository; + @Mock + private NotificationRepository notificationRepository; + @Mock + private MongoTemplate mongoTemplate; + @Mock + private ModelMapper modelMapper; + + @InjectMocks + private CommentService commentService; + @Mock + private NotificationService notificationService; + + private User user; + private Post post; + private Profile profile; + private Comment comment; + + @BeforeEach + void setUp() { + user = new User(); + user.setId("user123"); + user.setUsername("testuser"); + + post = new Post(); + post.setId("post123"); + post.setTitle("Test Post"); + + profile = new Profile(); + profile.setUserId("user123"); + + comment = new Comment(); + comment.setCommentContent("Test Comment"); + comment.setCommenter(user.getId()); // Set the commenter ID + } + + @Test + void createCommentTest() { + CreateCommentRequestDto request = new CreateCommentRequestDto(); + request.setPost(post.getId()); + + when(postRepository.findById(request.getPost())).thenReturn(Optional.of(post)); + when(mongoTemplate.findOne(any(), eq(Profile.class))).thenReturn(profile); + + Comment commentMock = new Comment(); + commentMock.setCommentContent("Test Comment"); + when(modelMapper.map(any(CreateCommentRequestDto.class), eq(Comment.class))).thenReturn(commentMock); + + when(commentRepository.save(any(Comment.class))).thenReturn(comment); + + Comment createdComment = commentService.createComment(request, user); + + assertNotNull(createdComment); + verify(commentRepository).save(any(Comment.class)); + } + + @Test + void createCommentPostNotFoundTest() { + CreateCommentRequestDto request = new CreateCommentRequestDto(); + request.setPost("invalidPostId"); + + when(postRepository.findById(anyString())).thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, () -> { + commentService.createComment(request, user); + }); + } + + + @Test + void replyCommentTest() { + ReplyCommentRequestDto request = new ReplyCommentRequestDto(); + request.setParentComment(comment.getId()); + + when(commentRepository.findById(request.getParentComment())).thenReturn(Optional.of(comment)); + when(mongoTemplate.findOne(any(), eq(Profile.class))).thenReturn(profile); + + Comment replyCommentMock = new Comment(); + replyCommentMock.setCommentContent("Reply Comment"); + when(modelMapper.map(any(ReplyCommentRequestDto.class), eq(Comment.class))).thenReturn(replyCommentMock); + + when(commentRepository.save(any(Comment.class))).thenReturn(replyCommentMock); + + Comment repliedComment = commentService.replyComment(request, user); + + assertNotNull(repliedComment); + verify(commentRepository).save(any(Comment.class)); + } + + @Test + void editCommentTest() { + String commentId = comment.getId(); + EditCommentRequestDto request = new EditCommentRequestDto(); + request.setCommentContent("Updated Comment"); + + when(commentRepository.findById(commentId)).thenReturn(Optional.of(comment)); + when(commentRepository.save(any(Comment.class))).thenReturn(comment); + + Comment editedComment = commentService.editComment(commentId, request, user); + + assertNotNull(editedComment); + assertEquals("Updated Comment", editedComment.getCommentContent()); + verify(commentRepository).save(any(Comment.class)); + } + + @Test + void deleteCommentTest() { + String commentId = comment.getId(); + + when(commentRepository.findById(commentId)).thenReturn(Optional.of(comment)); + when(mongoTemplate.findOne(any(), eq(Profile.class))).thenReturn(profile); + when(commentRepository.save(any(Comment.class))).thenReturn(comment); + + Comment deletedComment = commentService.deleteComment(commentId, user); + + assertNotNull(deletedComment); + assertTrue(deletedComment.getIsDeleted()); + verify(commentRepository).save(any(Comment.class)); + } + + @Test + void getUserCommentListTest() { + List comments = Arrays.asList(comment); + when(mongoTemplate.find(any(Query.class), eq(Comment.class))).thenReturn(comments); + + List userComments = commentService.getUserCommentList(user); + + assertNotNull(userComments); + assertFalse(userComments.isEmpty()); + assertEquals(1, userComments.size()); + assertEquals(comment, userComments.get(0)); + } +} diff --git a/app/backend/src/test/java/com/app/gamereview/service/ForumServiceTest.java b/app/backend/src/test/java/com/app/gamereview/service/ForumServiceTest.java new file mode 100644 index 00000000..fb3ec6e8 --- /dev/null +++ b/app/backend/src/test/java/com/app/gamereview/service/ForumServiceTest.java @@ -0,0 +1,110 @@ +package com.app.gamereview.service; + +import com.app.gamereview.enums.ForumType; +import com.app.gamereview.exception.ResourceNotFoundException; +import com.app.gamereview.model.Forum; +import com.app.gamereview.repository.ForumRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.modelmapper.ModelMapper; +import org.springframework.data.mongodb.core.MongoTemplate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ForumServiceTest { + + @Mock + private ForumRepository forumRepository; + + @Mock + private MongoTemplate mongoTemplate; + + @Mock + private ModelMapper modelMapper; + + @InjectMocks + private ForumService forumService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void banUser_SuccessfullyBanned() { + // Arrange + String forumId = "forum123"; + String userId = "user456"; + Forum forum = new Forum(); + forum.setId(forumId); + forum.setName("ForumName"); + forum.setType(ForumType.GROUP); + forum.setParent("ParentGroup"); + forum.setSubscribers(new ArrayList<>()); + forum.setBannedUsers(new ArrayList<>()); + when(forumRepository.findById(forumId)).thenReturn(Optional.of(forum)); + + // Act + boolean result = forumService.banUser(forumId, userId); + + // Assert + assertTrue(result); + assertTrue(forum.getBannedUsers().contains(userId)); + verify(forumRepository, times(1)).save(forum); + } + + @Test + void banUser_ForumNotFound() { + // Arrange + String forumId = "nonExistentForum"; + String userId = "user123"; + when(forumRepository.findById(forumId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> forumService.banUser(forumId, userId)); + verify(forumRepository, never()).save(any()); + } + + @Test + void unbanUser_SuccessfullyUnbanned() { + // Arrange + String forumId = "forum789"; + String userId = "user987"; + Forum forum = new Forum(); + forum.setId(forumId); + forum.setName("AnotherForum"); + forum.setType(ForumType.GAME); + forum.setParent("ParentGame"); + forum.setSubscribers(new ArrayList<>()); + forum.setBannedUsers(new ArrayList<>(List.of(userId))); + when(forumRepository.findById(forumId)).thenReturn(Optional.of(forum)); + + // Act + boolean result = forumService.unbanUser(forumId, userId); + + // Assert + assertTrue(result); + assertFalse(forum.getBannedUsers().contains(userId)); + verify(forumRepository, times(1)).save(forum); + } + + @Test + void unbanUser_ForumNotFound() { + // Arrange + String forumId = "nonExistentForum"; + String userId = "user789"; + when(forumRepository.findById(forumId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(ResourceNotFoundException.class, () -> forumService.unbanUser(forumId, userId)); + verify(forumRepository, never()).save(any()); + } +} diff --git a/app/backend/src/test/java/com/app/gamereview/service/GameServiceTest.java b/app/backend/src/test/java/com/app/gamereview/service/GameServiceTest.java new file mode 100644 index 00000000..a4d12f92 --- /dev/null +++ b/app/backend/src/test/java/com/app/gamereview/service/GameServiceTest.java @@ -0,0 +1,606 @@ +package com.app.gamereview.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.*; + +import com.app.gamereview.dto.request.game.*; +import com.app.gamereview.dto.response.game.GameDetailResponseDto; +import com.app.gamereview.dto.response.game.GetGameListResponseDto; +import com.app.gamereview.dto.response.tag.AddGameTagResponseDto; +import com.app.gamereview.dto.response.tag.GetAllTagsOfGameResponseDto; +import com.app.gamereview.enums.TagType; +import com.app.gamereview.exception.ResourceNotFoundException; +import com.app.gamereview.model.*; +import com.app.gamereview.repository.*; +import org.bson.Document; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.modelmapper.ModelMapper; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.query.Query; + +@ExtendWith(MockitoExtension.class) +class GameServiceTest { + + @Mock + private GameRepository gameRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private TagRepository tagRepository; + + @Mock + private ForumRepository forumRepository; + + @Mock + private ProfileRepository profileRepository; + + @Mock + private MongoTemplate mongoTemplate; + + @Mock + private ModelMapper modelMapper; + + @InjectMocks + private GameService gameService; + + @Test + void testCreateGame() { + // Arrange + CreateGameRequestDto requestDto = new CreateGameRequestDto(); + requestDto.setGameName("Test Game"); + requestDto.setDeveloper("developer"); + + List artstyles = new ArrayList<>(); + artstyles.add("art"); + + List other = new ArrayList<>(); + other.add("other"); + + requestDto.setArtStyles(artstyles); + requestDto.setOtherTags(other); + + + List playerTypes = new ArrayList<>(); + playerTypes.add("single"); + requestDto.setPlayerTypes(playerTypes); + + List genres = new ArrayList<>(); + genres.add("mmorpg"); + requestDto.setGenre(genres); + + List platforms = new ArrayList<>(); + platforms.add("ps3"); + requestDto.setPlatforms(platforms); + // Add other required fields in the requestDto + + Tag developer = new Tag(); + developer.setTagType(TagType.DEVELOPER); + + Tag single = new Tag(); + single.setTagType(TagType.PLAYER_TYPE); + + Tag mmorpg = new Tag(); + mmorpg.setTagType(TagType.GENRE); + + Tag ps3 = new Tag(); + ps3.setTagType(TagType.PLATFORM); + + Tag art = new Tag(); + art.setTagType(TagType.ART_STYLE); + + Tag otherTag = new Tag(); + otherTag.setTagType(TagType.OTHER); + + Game gameToCreate = new Game(); + gameToCreate.setGameName("Test Game"); + + // Mock the repository calls + when(gameRepository.findByGameNameAndIsDeletedFalse("Test Game")).thenReturn(Optional.empty()); + when(tagRepository.findByIdAndIsDeletedFalse(eq("developer"))).thenReturn(Optional.of(developer)); + when(tagRepository.findByIdAndIsDeletedFalse(eq("single"))).thenReturn(Optional.of(single)); + when(tagRepository.findByIdAndIsDeletedFalse(eq("mmorpg"))).thenReturn(Optional.of(mmorpg)); + when(tagRepository.findByIdAndIsDeletedFalse(eq("ps3"))).thenReturn(Optional.of(ps3)); + when(tagRepository.findByIdAndIsDeletedFalse(eq("art"))).thenReturn(Optional.of(art)); + when(tagRepository.findByIdAndIsDeletedFalse(eq("other"))).thenReturn(Optional.of(otherTag)); + when(modelMapper.map(any(CreateGameRequestDto.class), eq(Game.class))).thenReturn(gameToCreate); + when(gameRepository.save(any(Game.class))).thenReturn(gameToCreate); + + // Act + Game createdGame = gameService.createGame(requestDto); + + // Assert + assertNotNull(createdGame); + assertEquals("Test Game", createdGame.getGameName()); + + // Verify repository method calls + verify(gameRepository, times(1)).findByGameNameAndIsDeletedFalse("Test Game"); + verify(tagRepository, times(6)).findByIdAndIsDeletedFalse(any()); + verify(gameRepository, times(1)).save(any(Game.class)); + verify(forumRepository, times(1)).save(any(Forum.class)); + } + + @Test + void testGetGameTags() { + // Arrange + String gameId = "123"; + List artTags = Arrays.asList( + new Tag("Art1", TagType.ART_STYLE, "blue"), + new Tag("Art2", TagType.ART_STYLE, "red") + ); + + Tag developer = new Tag("developer", TagType.DEVELOPER, "blue"); + + Game game = new Game(); + game.addTag(developer); + game.addTag(artTags.get(0)); + game.addTag(artTags.get(1)); + + // Mock the repository call + when(gameRepository.findById(eq(gameId))).thenReturn(Optional.of(game)); + + // Act + GetAllTagsOfGameResponseDto actualTags = gameService.getGameTags(gameId); + + List artTagIds = new ArrayList<>(); + + for (Tag t : artTags) { + artTagIds.add(t.getId()); + } + + // Assert + assertEquals(artTagIds, game.getArtStyles()); + assertEquals(developer.getId(), game.getDeveloper()); + + + assertEquals(actualTags.getPlayerTypes(), new ArrayList<>()); + assertEquals(actualTags.getGenre(), new ArrayList<>()); + assertNull(actualTags.getProduction()); + assertNull(actualTags.getDuration()); + assertEquals(actualTags.getPlatforms(), new ArrayList<>()); + assertEquals(actualTags.getOtherTags(), new ArrayList<>()); + + // Verify repository method call + verify(gameRepository, times(1)).findById(eq(gameId)); + } + + @Test + void testAddGameTag() { + + AddGameTagRequestDto request = new AddGameTagRequestDto("game", "tag"); + + Game game = new Game(); + + Tag tag = new Tag(); + tag.setTagType(TagType.DEVELOPER); + + when(gameRepository.findById(eq("game"))).thenReturn(Optional.of(game)); + when(tagRepository.findById(eq("tag"))).thenReturn(Optional.of(tag)); + + AddGameTagResponseDto response = gameService.addGameTag(request); + + assertEquals(response.getGameId(), game.getId()); + assertEquals(response.getAddedTag(), tag); + + verify(gameRepository, times(1)).findById(eq("game")); + verify(gameRepository, times(1)).save(eq(game)); + verify(tagRepository, times(1)).findById(eq("tag")); + } + + @Test + void testRemoveGameTag() { + + RemoveGameTagRequestDto request = new RemoveGameTagRequestDto("game", "tag"); + + Game game = new Game(); + + Tag tag = new Tag(); + tag.setTagType(TagType.DEVELOPER); + + game.addTag(tag); + + when(gameRepository.findById(eq("game"))).thenReturn(Optional.of(game)); + when(tagRepository.findById(eq("tag"))).thenReturn(Optional.of(tag)); + + Boolean response = gameService.removeGameTag(request); + + assertTrue(response); + + verify(gameRepository, times(1)).findById(eq("game")); + verify(gameRepository, times(1)).save(eq(game)); + verify(tagRepository, times(1)).findById(eq("tag")); + + } + + @Test + void testRecommendationByGameId() { + // Arrange + String gameId = "game"; + Game baseGame = new Game(); // Set up your base game + baseGame.setId(gameId); + baseGame.setGameName("gameName"); + when(gameRepository.findByIdAndIsDeletedFalse(gameId)).thenReturn(Optional.of(baseGame)); + + // Mock the MongoDB template to return a list of similar games + when(mongoTemplate.find(any(Query.class), eq(Game.class))) + .thenReturn(Arrays.asList(createGame("Game1"), createGame("Game2"))); + + // Mock other dependencies as needed + + // Act + TreeSet result = gameService.recommendationByGameId(gameId); + + // Assert + assertNotNull(result); + // Add more specific assertions based on your expected behavior + + // Verify that certain methods were called with expected parameters + verify(gameRepository).findByIdAndIsDeletedFalse(gameId); + verify(mongoTemplate, times(2)).find(any(Query.class), eq(Game.class)); + } + + @Test + void testGetGameDetailWhenGameExists() { + // Arrange + String gameId = "123"; + Game game = new Game(); // Assuming you have a Game entity with necessary properties + game.setId(gameId); + + Tag tag = new Tag("tag", TagType.DEVELOPER, "blue"); + game.addTag(tag); + + GameDetailResponseDto expectedResponse = new GameDetailResponseDto(); + expectedResponse.setId(gameId); + + when(gameRepository.findById(gameId)).thenReturn(Optional.of(game)); + when(tagRepository.findById(any())).thenReturn(Optional.of(tag)); // Assuming Tag entity is available + when(modelMapper.map(eq(game), eq(GameDetailResponseDto.class))).thenReturn(expectedResponse); + + // Act + GameDetailResponseDto actualResponse = gameService.getGameDetail(gameId); + + // Assert + assertEquals(expectedResponse, actualResponse); + verify(gameRepository, times(1)).findById(gameId); + verify(tagRepository, times(1)).findById(any()); + verify(modelMapper, times(1)).map(eq(game), eq(GameDetailResponseDto.class)); + } + + @Test + void testGetGameByNameWhenGameExists() { + // Arrange + String gameName = "TestGame"; + Game game = new Game(); // Assuming you have a Game entity with necessary properties + game.setGameName(gameName); + + Tag tag = new Tag("tag", TagType.DEVELOPER, "blue"); + game.addTag(tag); + + GameDetailResponseDto expectedResponse = new GameDetailResponseDto(); + expectedResponse.setGameName(gameName); + + when(gameRepository.findByGameNameAndIsDeletedFalse(gameName)).thenReturn(Optional.of(game)); + when(tagRepository.findById(any())).thenReturn(Optional.of(tag)); // Assuming Tag entity is available + when(modelMapper.map(eq(game), eq(GameDetailResponseDto.class))).thenReturn(expectedResponse); + + // Act + GameDetailResponseDto actualResponse = gameService.getGameByName(gameName); + + // Assert + assertEquals(expectedResponse, actualResponse); + verify(gameRepository, times(1)).findByGameNameAndIsDeletedFalse(gameName); + verify(tagRepository, times(1)).findById(any()); + verify(modelMapper, times(1)).map(eq(game), eq(GameDetailResponseDto.class)); + } + + @Test + void testGetRecommendedGames() { + // Arrange + String userEmail = "test@example.com"; + User user = new User(); // Set up your user + + String gameId = "game"; + Game baseGame = new Game(); // Set up your base game + baseGame.setId(gameId); + baseGame.setGameName("gameName"); + + Profile profile = new Profile(); // Set up your profile + profile.addGame(gameId); + + when(userRepository.findByEmailAndIsDeletedFalse(userEmail)).thenReturn(Optional.of(user)); + + when(profileRepository.findByUserIdAndIsDeletedFalse(user.getId())).thenReturn(Optional.of(profile)); + + when(gameRepository.findByIdAndIsDeletedFalse(gameId)).thenReturn(Optional.of(baseGame)); + + // Mock the MongoDB template to return a list of similar games + when(mongoTemplate.find(any(Query.class), eq(Game.class))) + .thenReturn(Arrays.asList(createGame("Game1"), createGame("Game2"))); + + + // Act + List result = gameService.getRecommendedGames(userEmail); + + // Assert + assertNotNull(result); + // Add more specific assertions based on your expected behavior + + // Verify that certain methods were called with expected parameters + verify(userRepository).findByEmailAndIsDeletedFalse(userEmail); + verify(profileRepository).findByUserIdAndIsDeletedFalse(user.getId()); + verify(gameRepository).findByIdAndIsDeletedFalse(gameId); + verify(mongoTemplate, times(3)).find(any(Query.class), eq(Game.class)); + } + + @Test + void testGetRecommendedGamesGuest() { + // Arrange + List expectedGames = Arrays.asList( + createGame("Game1"), + createGame("Game2"), + createGame("Game3") + // Add more games as needed + ); + + when(mongoTemplate.find(any(Query.class), eq(Game.class))).thenReturn(expectedGames); + + // Act + List actualGames = gameService.getRecommendedGames(); + + // Assert + assertEquals(expectedGames, actualGames); + } + + @Test + void testEditGameWhenGameExists() { + // Arrange + String gameId = "123"; + UpdateGameRequestDto updateRequest = new UpdateGameRequestDto(); + updateRequest.setGameName("UpdatedName"); + updateRequest.setGameDescription("UpdatedDescription"); + updateRequest.setGameIcon("UpdatedIcon"); + // Set other properties as needed + + Game existingGame = new Game(); + existingGame.setId(gameId); + + when(gameRepository.findByIdAndIsDeletedFalse(gameId)).thenReturn(Optional.of(existingGame)); + when(gameRepository.save(any())).thenReturn(existingGame); + + // Act + Game updatedGame = gameService.editGame(gameId, updateRequest); + + // Assert + assertEquals(updateRequest.getGameName(), updatedGame.getGameName()); + assertEquals(updateRequest.getGameDescription(), updatedGame.getGameDescription()); + assertEquals(updateRequest.getGameIcon(), updatedGame.getGameIcon()); + // Add additional assertions for other properties as needed + } + + @Test + void testEditGameWhenGameNotExists() { + // Arrange + String gameId = "NonExistentGame"; + UpdateGameRequestDto updateRequest = new UpdateGameRequestDto(); + + when(gameRepository.findByIdAndIsDeletedFalse(gameId)).thenReturn(Optional.empty()); + + // Act and Assert + assertThrows(ResourceNotFoundException.class, () -> gameService.editGame(gameId, updateRequest)); + } + + @Test + void testDeleteGameWhenGameExists() { + // Arrange + String gameId = "123"; + Game existingGame = new Game(); + existingGame.setId(gameId); + + when(gameRepository.findByIdAndIsDeletedFalse(gameId)).thenReturn(Optional.of(existingGame)); + when(gameRepository.save(any())).thenReturn(existingGame); + + // Act + boolean result = gameService.deleteGame(gameId); + + // Assert + assertTrue(result); + assertTrue(existingGame.getIsDeleted()); + } + + @Test + void testDeleteGameWhenGameNotExists() { + // Arrange + String gameId = "NonExistentGame"; + when(gameRepository.findByIdAndIsDeletedFalse(gameId)).thenReturn(Optional.empty()); + + // Act and Assert + assertThrows(ResourceNotFoundException.class, () -> gameService.deleteGame(gameId)); + } + + @Test + void testChangePromotionStatusOfGameWhenGameExists() { + // Arrange + String gameId = "123"; + Game existingGame = new Game(); + existingGame.setId(gameId); + existingGame.setIsPromoted(false); + + when(gameRepository.findByIdAndIsDeletedFalse(gameId)).thenReturn(Optional.of(existingGame)); + when(gameRepository.save(any())).thenReturn(existingGame); + + // Act + Game updatedGame = gameService.changePromotionStatusOfGame(gameId); + + // Assert + assertTrue(updatedGame.getIsPromoted()); + verify(gameRepository, times(1)).save(existingGame); + } + + @Test + void testChangePromotionStatusOfGameWhenGameNotExists() { + // Arrange + String gameId = "NonExistentGame"; + when(gameRepository.findByIdAndIsDeletedFalse(gameId)).thenReturn(Optional.empty()); + + // Act and Assert + assertThrows(ResourceNotFoundException.class, () -> gameService.changePromotionStatusOfGame(gameId)); + verify(gameRepository, never()).save(any()); + } + + @Test + void testCalculateSimilarityScore() { + // Arrange + Game basedGame = new Game(); + basedGame.setId("1"); + + List mockTags = Arrays.asList( + createTag("tag1", TagType.PRODUCTION), + createTag("tag2", TagType.GENRE), + createTag("tag3", TagType.GENRE), + createTag("tag4", TagType.PRODUCTION) + ); + + basedGame.addTag(mockTags.get(0)); + basedGame.addTag(mockTags.get(1)); + basedGame.addTag(mockTags.get(2)); + basedGame.addTag(mockTags.get(3)); + + Game candidateGame = new Game(); + candidateGame.setId("2"); + + candidateGame.addTag(mockTags.get(1)); + candidateGame.addTag(mockTags.get(2)); + candidateGame.addTag(mockTags.get(3)); + + when(tagRepository.findByIdAndIsDeletedFalse("tag2")).thenReturn(Optional.of(mockTags.get(1))); + when(tagRepository.findByIdAndIsDeletedFalse("tag3")).thenReturn(Optional.of(mockTags.get(2))); + when(tagRepository.findByIdAndIsDeletedFalse("tag4")).thenReturn(Optional.of(mockTags.get(3))); + + // Act + int similarityScore = gameService.calculateSimilarityScore(basedGame, candidateGame); + + // Assert + assertEquals(13, similarityScore); + } + + @Test + void testGetGames() { + // Arrange + GetGameListRequestDto filter = new GetGameListRequestDto(); + filter.setGameName("TestGame"); + filter.setFindDeleted(false); + + List playerTypes = new ArrayList<>(); + playerTypes.add("playerType"); + + List genres = new ArrayList<>(); + genres.add("genre"); + + List platforms = new ArrayList<>(); + platforms.add("platform"); + + List artstyles = new ArrayList<>(); + artstyles.add("art"); + + filter.setPlayerTypes(playerTypes); + filter.setGenre(genres); + filter.setProduction("aaa"); + filter.setPlatform(platforms); + filter.setArtStyle(artstyles); + + Game filtered1 = new Game(); + + Game filtered2 = new Game(); + + List filteredGames = Arrays.asList( + filtered1, + filtered2 + ); + + when(mongoTemplate.aggregate(any(Aggregation.class), eq("Game"), eq(Game.class))) + .thenReturn(new AggregationResults<>(Collections.emptyList(), new Document())); +// when(mongoTemplate.aggregate(any(Aggregation.class), eq("Game"), eq(Game.class)).getMappedResults()).thenReturn(promotedGames); + when(mongoTemplate.find(any(Query.class), eq(Game.class))).thenReturn(filteredGames); + + // Act + List result = gameService.getGames(filter); + + // Assert + assertEquals(2, result.size()); // 2 promoted games + 2 filtered games + verify(mongoTemplate, times(1)).aggregate(any(Aggregation.class), eq("Game"), eq(Game.class)); + verify(mongoTemplate, times(1)).find(any(Query.class), eq(Game.class)); + } + + @Test + void testGetAllGames() { + // Arrange + GetGameListRequestDto filter = new GetGameListRequestDto(); + filter.setGameName("TestGame"); + filter.setFindDeleted(false); + + List playerTypes = new ArrayList<>(); + playerTypes.add("playerType"); + + List genres = new ArrayList<>(); + genres.add("genre"); + + List platforms = new ArrayList<>(); + platforms.add("platform"); + + List artstyles = new ArrayList<>(); + artstyles.add("art"); + + filter.setPlayerTypes(playerTypes); + filter.setGenre(genres); + filter.setProduction("aaa"); + filter.setPlatform(platforms); + filter.setArtStyle(artstyles); + + Game filtered1 = new Game(); + + Game filtered2 = new Game(); + + List filteredGames = Arrays.asList( + filtered1, + filtered2 + ); + + when(mongoTemplate.aggregate(any(Aggregation.class), eq("Game"), eq(Game.class))) + .thenReturn(new AggregationResults<>(Collections.emptyList(), new Document())); +// when(mongoTemplate.aggregate(any(Aggregation.class), eq("Game"), eq(Game.class)).getMappedResults()).thenReturn(promotedGames); + when(mongoTemplate.find(any(Query.class), eq(Game.class))).thenReturn(filteredGames); + + // Act + List result = gameService.getAllGames(filter); + + // Assert + assertEquals(2, result.size()); // 2 promoted games + 2 filtered games + verify(mongoTemplate, times(1)).aggregate(any(Aggregation.class), eq("Game"), eq(Game.class)); + verify(mongoTemplate, times(1)).find(any(Query.class), eq(Game.class)); + } + + private Game createGame(String gameName) { + Game game = new Game(); + game.setGameName(gameName); + // Set other properties as needed + return game; + } + + private Tag createTag(String tagId, TagType type) { + Tag tag = new Tag(); + tag.setId(tagId); + tag.setTagType(type); + // Set other properties as needed + return tag; + } +} \ No newline at end of file diff --git a/app/backend/src/test/java/com/app/gamereview/service/GroupServiceTest.java b/app/backend/src/test/java/com/app/gamereview/service/GroupServiceTest.java new file mode 100644 index 00000000..9d2a9dcb --- /dev/null +++ b/app/backend/src/test/java/com/app/gamereview/service/GroupServiceTest.java @@ -0,0 +1,972 @@ +package com.app.gamereview.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.app.gamereview.dto.request.group.*; +import com.app.gamereview.dto.response.group.GetGroupDetailResponseDto; +import com.app.gamereview.dto.response.group.GetGroupResponseDto; +import com.app.gamereview.dto.response.group.GroupApplicationResponseDto; +import com.app.gamereview.dto.response.tag.AddGroupTagResponseDto; +import com.app.gamereview.enums.*; +import com.app.gamereview.exception.BadRequestException; +import com.app.gamereview.exception.ResourceNotFoundException; +import com.app.gamereview.model.*; +import com.app.gamereview.repository.*; +import com.app.gamereview.service.GroupService; +import com.app.gamereview.util.UtilExtensions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.modelmapper.ModelMapper; +import org.springframework.data.mongodb.core.MongoTemplate; + +import java.util.*; + +class GroupServiceTest { + + @Mock + private GroupRepository groupRepository; + + @Mock + private GameRepository gameRepository; + + @Mock + private ForumRepository forumRepository; + + @Mock + private TagRepository tagRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private ProfileRepository profileRepository; + + @Mock + private GroupApplicationRepository groupApplicationRepository; + + @Mock + private MongoTemplate mongoTemplate; + + @Mock + private ModelMapper modelMapper; + + @InjectMocks + private GroupService groupService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void createGroup_Success() { + // Arrange + CreateGroupRequestDto requestDto = new CreateGroupRequestDto(); + requestDto.setTitle("Test Group"); + requestDto.setTags(Arrays.asList("tag1", "tag2")); + requestDto.setGameId("gameId"); + + User user = new User(); + user.setId("userId"); + + Group groupToCreate = new Group(); + groupToCreate.setTitle("Test Group"); + //groupToCreate.setTags(Arrays.asList("tag1", "tag2")); + groupToCreate.setGameId("gameId"); + + //Forum correspondingForum = new Forum(); + + Tag tag1 = new Tag(); + tag1.setTagType(TagType.GROUP); + tag1.setId("tag1"); + + Tag tag2 = new Tag(); + tag2.setTagType(TagType.GROUP); + tag2.setId("tag2"); + Game game = new Game("Game 1", "Description 1", null, "SystemReq 1"); + game.setId("gameId"); + + Forum forum = new Forum(); + forum.setId("forumId"); + forum.setName("ForumName"); + forum.setType(ForumType.GROUP); + forum.setParent("ParentGroup"); + // Mock repository calls + when(groupRepository.findByTitleAndIsDeletedFalse("Test Group")).thenReturn(Optional.empty()); + when(tagRepository.findByIdAndIsDeletedFalse("tag1")).thenReturn(Optional.of(tag1)); + when(tagRepository.findByIdAndIsDeletedFalse("tag2")).thenReturn(Optional.of(tag2)); + when(gameRepository.findByIdAndIsDeletedFalse("gameId")).thenReturn(Optional.of(game)); + when(modelMapper.map(any(), any())).thenReturn(groupToCreate); + when(groupRepository.save(any())).thenReturn(groupToCreate); + when(forumRepository.save(any())).thenReturn(forum); + + // Act + Group createdGroup = groupService.createGroup(requestDto, user); + + // Assert + assertNotNull(createdGroup); + assertEquals("Test Group", createdGroup.getTitle()); + assertEquals("userId", createdGroup.getModerators().get(0)); + assertEquals("userId", createdGroup.getMembers().get(0)); + } + + @Test + void createGroup_GroupWithTitleExists_ThrowsBadRequestException() { + // Arrange + CreateGroupRequestDto requestDto = new CreateGroupRequestDto(); + requestDto.setTitle("Test Group"); + + User user = new User(); + + // Mock repository calls + when(groupRepository.findByTitleAndIsDeletedFalse("Test Group")).thenReturn(Optional.of(new Group())); + + // Act and Assert + assertThrows(BadRequestException.class, () -> groupService.createGroup(requestDto, user)); + } + + @Test + void createGroup_TagNotFound_ThrowsResourceNotFoundException() { + // Arrange + CreateGroupRequestDto requestDto = new CreateGroupRequestDto(); + requestDto.setTitle("Test Group"); + requestDto.setTags(Collections.singletonList("tag1")); + + User user = new User(); + + // Mock repository calls + when(groupRepository.findByTitleAndIsDeletedFalse("Test Group")).thenReturn(Optional.empty()); + when(tagRepository.findByIdAndIsDeletedFalse("tag1")).thenReturn(Optional.empty()); + + // Act and Assert + assertThrows(ResourceNotFoundException.class, () -> groupService.createGroup(requestDto, user)); + } + @Test + void testGetAllGroups() { + // Arrange + GetAllGroupsFilterRequestDto filter = new GetAllGroupsFilterRequestDto(); + filter.setTitle("Test Group"); + filter.setSortBy(SortType.CREATION_DATE.name()); + filter.setSortDirection(SortDirection.DESCENDING.name()); + filter.setWithDeleted(false); + + String email = "user@example.com"; + User loggedInUser = new User(); + loggedInUser.setId("1"); + loggedInUser.setEmail(email); + + when(userRepository.findByEmailAndIsDeletedFalse(email)).thenReturn(Optional.of(loggedInUser)); + + Group group = new Group(); + group.setId("1"); + group.setTitle("Test Group"); + group.setMembers(Arrays.asList("1", "2")); + + when(mongoTemplate.find(any(), eq(Group.class))).thenReturn(Arrays.asList(group)); + + GetGroupResponseDto responseDto = new GetGroupResponseDto(); + responseDto.setId("1"); + responseDto.setTitle("Test Group"); + responseDto.setUserJoined(true); + + when(modelMapper.map(group, GetGroupResponseDto.class)).thenReturn(responseDto); + + // Act + List result = groupService.getAllGroups(filter, email); + + // Assert + assertNotNull(result); + assertFalse(result.isEmpty()); + assertEquals(1, result.size()); + + GetGroupResponseDto resultDto = result.get(0); + assertEquals("1", resultDto.getId()); + assertEquals("Test Group", resultDto.getTitle()); + assertTrue(resultDto.getUserJoined()); + } + + // Add more test cases as needed based on different scenarios and edge cases. + @Test + void testGetGroupById() { + // Arrange + String groupId = "1"; + String email = "user@example.com"; + + User loggedInUser = new User(); + loggedInUser.setId("2"); + loggedInUser.setEmail(email); + + Profile profile = new Profile(); + profile.setUserId(loggedInUser.getId()); + + when(userRepository.findByEmailAndIsDeletedFalse(email)).thenReturn(Optional.of(loggedInUser)); + + Group group = new Group(); + group.setId(groupId); + group.setTitle("Test Group"); + group.setMembers(Arrays.asList("1", "2")); + group.setModerators(Arrays.asList("1")); + group.setTags(Arrays.asList("tag1")); + + when(userRepository.findByIdAndIsDeletedFalse(loggedInUser.getId())).thenReturn(Optional.of(loggedInUser)); + when(profileRepository.findByUserIdAndIsDeletedFalse(loggedInUser.getId())).thenReturn(Optional.of(profile)); + + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + + GetGroupDetailResponseDto responseDto = new GetGroupDetailResponseDto(); + responseDto.setId(groupId); + responseDto.setTitle("Test Group"); + responseDto.setUserJoined(true); + + + Tag tag = new Tag(); + tag.setId("tag1"); + tag.setName("Test Tag"); + + + when(tagRepository.findById("tag1")).thenReturn(Optional.of(tag)); + when(modelMapper.map(group, GetGroupDetailResponseDto.class)).thenReturn(responseDto); + + + // Act + GetGroupDetailResponseDto result = groupService.getGroupById(groupId, email); + System.out.println(result.getTags()); + // Assert + assertNotNull(result); + assertEquals(groupId, result.getId()); + assertEquals("Test Group", result.getTitle()); + assertTrue(result.getUserJoined()); + assertNotNull(result.getTags()); + assertEquals(1, result.getTags().size()); + assertEquals("tag1", result.getTags().get(0).getId()); + assertEquals("Test Tag", result.getTags().get(0).getName()); + //assertNotNull(result.getMembers()); + //assertEquals(2, result.getMembers().size()); + //assertNotNull(result.getModerators()); + //assertEquals(1, result.getModerators().size()); + } + + @Test + void testGetGroupByIdNotFound() { + // Arrange + String groupId = "1"; + String email = "user@example.com"; + + when(userRepository.findByEmailAndIsDeletedFalse(email)).thenReturn(Optional.of(new User())); + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.empty()); + + // Act and Assert + assertThrows(ResourceNotFoundException.class, () -> groupService.getGroupById(groupId, email)); + } + @Test + void testDeleteGroupByIdentifier() { + // Arrange + String groupId = "1"; + + Group group = new Group(); + group.setId(groupId); + group.setTitle("Test Group"); + group.setIsDeleted(false); + + when(groupRepository.findByTitleAndIsDeletedFalse(group.getTitle())).thenReturn(Optional.of(group)); + + // Act + boolean result = groupService.deleteGroup(group.getTitle()); + + // Assert + assertTrue(result); + assertTrue(group.getIsDeleted()); + verify(groupRepository, times(1)).save(group); + } + + @Test + void testDeleteGroupByTitle() { + // Arrange + String groupTitle = "Test Group"; + + Group group = new Group(); + group.setId("1"); + group.setTitle(groupTitle); + group.setIsDeleted(false); + + when(groupRepository.findByTitleAndIsDeletedFalse(groupTitle)).thenReturn(Optional.of(group)); + + // Act + boolean result = groupService.deleteGroup(groupTitle); + + // Assert + assertTrue(result); + assertTrue(group.getIsDeleted()); + verify(groupRepository, times(1)).save(group); + } + + @Test + void testDeleteGroupNotFound() { + // Arrange + String identifier = "nonexistent"; + + when(groupRepository.findByIdAndIsDeletedFalse(identifier)).thenReturn(Optional.empty()); + + // Act and Assert + assertThrows(ResourceNotFoundException.class, () -> groupService.deleteGroup(identifier)); + } + + @Test + void testUpdateGroup() { + // Arrange + String groupId = "1"; + UpdateGroupRequestDto request = new UpdateGroupRequestDto(); + request.setTitle("Updated Title"); + request.setDescription("Updated Description"); + request.setMembershipPolicy(MembershipPolicy.PUBLIC.name()); + request.setQuota(100); + request.setAvatarOnly(true); + + Group group = new Group(); + group.setId(groupId); + group.setTitle("Original Title"); + group.setDescription("Original Description"); + group.setMembershipPolicy(MembershipPolicy.PRIVATE); + group.setQuota(50); + group.setAvatarOnly(false); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + + // Act + Group updatedGroup = groupService.updateGroup(groupId, request); + + // Assert + assertNotNull(updatedGroup); + assertEquals(groupId, updatedGroup.getId()); + assertEquals("Updated Title", updatedGroup.getTitle()); + assertEquals("Updated Description", updatedGroup.getDescription()); + assertEquals(MembershipPolicy.PUBLIC, updatedGroup.getMembershipPolicy()); + assertEquals(100, updatedGroup.getQuota()); + assertTrue(updatedGroup.getAvatarOnly()); + verify(groupRepository, times(1)).save(group); + } + + @Test + void testUpdateGroupNotFound() { + // Arrange + String groupId = "nonexistent"; + UpdateGroupRequestDto request = new UpdateGroupRequestDto(); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.empty()); + + // Act and Assert + assertThrows(ResourceNotFoundException.class, () -> groupService.updateGroup(groupId, request)); + } + + @Test + void testUpdateGroupInvalidQuota() { + // Arrange + String groupId = "1"; + UpdateGroupRequestDto request = new UpdateGroupRequestDto(); + request.setQuota(1); // Assuming current members count is 50 + + Group group = new Group(); + group.setId(groupId); + group.setMembers(List.of("member1", "member2")); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + + // Act and Assert + assertThrows(BadRequestException.class, () -> groupService.updateGroup(groupId, request)); + } + + @Test + void testJoinGroup() { + // Arrange + String groupId = "1"; + User user = new User(); + user.setId("user1"); + + Group group = new Group(); + group.setId(groupId); + group.setMembershipPolicy(MembershipPolicy.PUBLIC); + group.setQuota(5); + group.setMembers(new ArrayList<>(List.of("user1"))); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + + // Act + boolean result = groupService.joinGroup(groupId, user); + + // Assert + assertTrue(result); + assertTrue(group.getMembers().contains("user1")); + verify(groupRepository, times(1)).save(group); + } + + @Test + void testJoinPrivateGroup() { + // Arrange + String groupId = "1"; + User user = new User(); + user.setId("user1"); + + Group group = new Group(); + group.setId(groupId); + group.setMembershipPolicy(MembershipPolicy.PRIVATE); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + + // Act and Assert + assertThrows(BadRequestException.class, () -> groupService.joinGroup(groupId, user)); + } + + @Test + void testJoinFullGroup() { + // Arrange + String groupId = "1"; + User user = new User(); + user.setId("user1"); + + Group group = new Group(); + group.setId(groupId); + group.setMembershipPolicy(MembershipPolicy.PUBLIC); + group.setQuota(2); + group.setMembers(List.of("user2", "user3")); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + + // Act and Assert + assertThrows(BadRequestException.class, () -> groupService.joinGroup(groupId, user)); + } + + @Test + void testLeaveGroup() { + // Arrange + String groupId = "1"; + User user = new User(); + user.setId("user1"); + + Group group = new Group(); + group.setId(groupId); + group.setMembers(new ArrayList<>(List.of("user1", "user2", "user3"))); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + + // Act + boolean result = groupService.leaveGroup(groupId, user); + + // Assert + assertTrue(result); + assertFalse(group.getMembers().contains("user1")); + verify(groupRepository, times(1)).save(group); + } + + @Test + void testLeaveGroupNotFound() { + // Arrange + String groupId = "nonexistent"; + User user = new User(); + user.setId("user1"); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.empty()); + + // Act and Assert + assertThrows(ResourceNotFoundException.class, () -> groupService.leaveGroup(groupId, user)); + } + + @Test + void testApplyGroup() { + // Arrange + String groupId = "1"; + User user = new User(); + user.setId("user1"); + + Group group = new Group(); + group.setId(groupId); + group.setMembershipPolicy(MembershipPolicy.PRIVATE); + group.setMembers(List.of("user2", "user3")); + group.setBannedMembers(List.of("user4")); + + GroupApplicationRequestDto requestDto = new GroupApplicationRequestDto(); + requestDto.setMessage("Application message"); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + when(groupApplicationRepository.findByUserAndGroupAndStatus("user1", groupId, GroupApplicationStatus.PENDING)) + .thenReturn(Optional.empty()); + + // Act + boolean result = groupService.applyGroup(groupId, user, requestDto); + + // Assert + assertTrue(result); + verify(groupApplicationRepository, times(1)).save(any(GroupApplication.class)); + } + + @Test + void testApplyGroupNotFound() { + // Arrange + String groupId = "nonexistent"; + User user = new User(); + user.setId("user1"); + GroupApplicationRequestDto requestDto = new GroupApplicationRequestDto(); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.empty()); + + // Act and Assert + assertThrows(ResourceNotFoundException.class, () -> groupService.applyGroup(groupId, user, requestDto)); + } + + @Test + void testApplyGroupPublicGroup() { + // Arrange + String groupId = "1"; + User user = new User(); + user.setId("user1"); + + Group group = new Group(); + group.setId(groupId); + group.setMembershipPolicy(MembershipPolicy.PUBLIC); + + GroupApplicationRequestDto requestDto = new GroupApplicationRequestDto(); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + + // Act and Assert + assertThrows(BadRequestException.class, () -> groupService.applyGroup(groupId, user, requestDto)); + } + + @Test + void testApplyGroupAlreadyMember() { + // Arrange + String groupId = "1"; + User user = new User(); + user.setId("user1"); + + Group group = new Group(); + group.setId(groupId); + group.setMembers(List.of("user1")); + + GroupApplicationRequestDto requestDto = new GroupApplicationRequestDto(); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + + // Act and Assert + assertThrows(BadRequestException.class, () -> groupService.applyGroup(groupId, user, requestDto)); + } + + @Test + void testApplyGroupBannedMember() { + // Arrange + String groupId = "1"; + User user = new User(); + user.setId("user1"); + + Group group = new Group(); + group.setId(groupId); + group.setBannedMembers(List.of("user1")); + + GroupApplicationRequestDto requestDto = new GroupApplicationRequestDto(); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + + // Act and Assert + assertThrows(BadRequestException.class, () -> groupService.applyGroup(groupId, user, requestDto)); + } + + @Test + void testApplyGroupPendingRequest() { + // Arrange + String groupId = "1"; + User user = new User(); + user.setId("user1"); + + Group group = new Group(); + group.setId(groupId); + + GroupApplicationRequestDto requestDto = new GroupApplicationRequestDto(); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + when(groupApplicationRepository.findByUserAndGroupAndStatus("user1", groupId, GroupApplicationStatus.PENDING)) + .thenReturn(Optional.of(new GroupApplication())); + + // Act and Assert + assertThrows(BadRequestException.class, () -> groupService.applyGroup(groupId, user, requestDto)); + } + + @Test + void testReviewApplicationApprove() { + // Arrange + String applicationId = "1"; + User user = new User(); + user.setId("moderator1"); + + + GroupApplicationReviewDto reviewDto = new GroupApplicationReviewDto(); + reviewDto.setResult(GroupApplicationReviewResult.APPROVE.name()); + + GroupApplication application = new GroupApplication(); + application.setId(applicationId); + application.setStatus(GroupApplicationStatus.PENDING); + application.setUser("user1"); + application.setGroup("groupId"); + + Group appliedGroup = new Group(); + appliedGroup.setId("groupId"); + appliedGroup.setMembers(new ArrayList<>(List.of("user2", "user3"))); + appliedGroup.setQuota(5); + appliedGroup.setModerators(new ArrayList<>(List.of("moderator1"))); + appliedGroup.setBannedMembers(new ArrayList<>(List.of("user4"))); + + when(groupApplicationRepository.findById(applicationId)).thenReturn(Optional.of(application)); + when(groupRepository.findByIdAndIsDeletedFalse("groupId")).thenReturn(Optional.of(appliedGroup)); + + // Act + boolean result = groupService.reviewApplication(applicationId, user, reviewDto); + + // Assert + assertTrue(result); + assertEquals(GroupApplicationStatus.APPROVED, application.getStatus()); + assertEquals("moderator1", application.getReviewer()); + assertNotNull(application.getReviewedAt()); + assertTrue(appliedGroup.getMembers().contains("user1")); + verify(groupRepository, times(1)).save(appliedGroup); + verify(groupApplicationRepository, times(1)).save(application); + } + + @Test + void testReviewApplicationReject() { + // Arrange + String applicationId = "1"; + User user = new User(); + user.setId("moderator1"); + + GroupApplicationReviewDto reviewDto = new GroupApplicationReviewDto(); + reviewDto.setResult(GroupApplicationReviewResult.REJECT.name()); + + GroupApplication application = new GroupApplication(); + application.setId(applicationId); + application.setStatus(GroupApplicationStatus.PENDING); + application.setUser("user1"); + application.setGroup("groupId"); + + Group appliedGroup = new Group(); + appliedGroup.setId("groupId"); + appliedGroup.setMembers(new ArrayList<>(List.of("user2", "user3"))); + appliedGroup.setQuota(5); + appliedGroup.setModerators(new ArrayList<>(List.of("moderator1"))); + appliedGroup.setBannedMembers(new ArrayList<>(List.of("user4"))); + + when(groupApplicationRepository.findById(applicationId)).thenReturn(Optional.of(application)); + when(groupRepository.findByIdAndIsDeletedFalse("groupId")).thenReturn(Optional.of(appliedGroup)); + + + // Act + boolean result = groupService.reviewApplication(applicationId, user, reviewDto); + + // Assert + assertTrue(result); + assertEquals(GroupApplicationStatus.REJECTED, application.getStatus()); + assertEquals("moderator1", application.getReviewer()); + assertNotNull(application.getReviewedAt()); + verify(groupApplicationRepository, times(1)).save(application); + } + + @Test + void testReviewApplicationNotModerator() { + // Arrange + String applicationId = "1"; + User user = new User(); + user.setId("nonModerator"); + + GroupApplicationReviewDto reviewDto = new GroupApplicationReviewDto(); + reviewDto.setResult(GroupApplicationReviewResult.APPROVE.name()); + + GroupApplication application = new GroupApplication(); + application.setId(applicationId); + application.setStatus(GroupApplicationStatus.PENDING); + application.setGroup("groupId"); + + Group appliedGroup = new Group(); + appliedGroup.setId("groupId"); + appliedGroup.setModerators(List.of("moderator1")); + + when(groupApplicationRepository.findById(applicationId)).thenReturn(Optional.of(application)); + when(groupRepository.findByIdAndIsDeletedFalse("groupId")).thenReturn(Optional.of(appliedGroup)); + + // Act and Assert + assertThrows(BadRequestException.class, () -> groupService.reviewApplication(applicationId, user, reviewDto)); + } + + @Test + void testListApplications() { + // Arrange + String groupId = "1"; + User user = new User(); + user.setId("moderator1"); + + Group group = new Group(); + group.setId(groupId); + group.setModerators(List.of("moderator1")); + + GroupApplication application1 = new GroupApplication(); + application1.setId("app1"); + application1.setUser("user1"); + application1.setGroup(groupId); + application1.setStatus(GroupApplicationStatus.PENDING); + + GroupApplication application2 = new GroupApplication(); + application2.setId("app2"); + application2.setUser("user2"); + application2.setGroup(groupId); + application2.setStatus(GroupApplicationStatus.PENDING); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + when(groupApplicationRepository.findByGroupAndStatus(groupId, GroupApplicationStatus.PENDING)) + .thenReturn(List.of(application1, application2)); + + // Act + List responseDtos = groupService.listApplications(groupId, user); + + // Assert + assertNotNull(responseDtos); + assertEquals(2, responseDtos.size()); + assertEquals("app1", responseDtos.get(0).getId()); + assertEquals(GroupApplicationStatus.PENDING, responseDtos.get(0).getStatus()); + assertEquals("app2", responseDtos.get(1).getId()); + assertEquals(GroupApplicationStatus.PENDING, responseDtos.get(1).getStatus()); + } + + @Test + void testListApplicationsGroupNotFound() { + // Arrange + String groupId = "nonexistent"; + User user = new User(); + user.setId("moderator1"); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.empty()); + + // Act and Assert + assertThrows(ResourceNotFoundException.class, () -> groupService.listApplications(groupId, user)); + verify(groupApplicationRepository, never()).findByGroupAndStatus(anyString(), any()); + } + + @Test + void testListApplicationsNotModerator() { + // Arrange + String groupId = "1"; + User user = new User(); + user.setId("nonModerator"); + + Group group = new Group(); + group.setId(groupId); + group.setModerators(List.of("moderator1")); + + when(groupRepository.findByIdAndIsDeletedFalse(groupId)).thenReturn(Optional.of(group)); + + // Act and Assert + assertThrows(BadRequestException.class, () -> groupService.listApplications(groupId, user)); + verify(groupApplicationRepository, never()).findByGroupAndStatus(anyString(), any()); + } + + @Test + void testAddGroupTag() { + // Arrange + AddGroupTagRequestDto requestDto = new AddGroupTagRequestDto(); + requestDto.setGroupId("groupId"); + requestDto.setTagId("tagId"); + + Group group = new Group(); + group.setId("groupId"); + + Tag tag = new Tag(); + tag.setId("tagId"); + tag.setTagType(TagType.GROUP); + + when(groupRepository.findById("groupId")).thenReturn(Optional.of(group)); + when(tagRepository.findById("tagId")).thenReturn(Optional.of(tag)); + + // Act + AddGroupTagResponseDto responseDto = groupService.addGroupTag(requestDto); + + // Assert + assertNotNull(responseDto); + assertEquals("groupId", responseDto.getGroupId()); + assertEquals(tag, responseDto.getAddedTag()); + assertTrue(group.getTags().contains("tagId")); + verify(groupRepository, times(1)).save(group); + } + + @Test + void testAddGroupTagTagNotGroupType() { + // Arrange + AddGroupTagRequestDto requestDto = new AddGroupTagRequestDto(); + requestDto.setGroupId("groupId"); + requestDto.setTagId("tagId"); + + Group group = new Group(); + group.setId("groupId"); + + Tag tag = new Tag(); + tag.setId("tagId"); + tag.setTagType(TagType.PLATFORM); + + when(groupRepository.findById("groupId")).thenReturn(Optional.of(group)); + when(tagRepository.findById("tagId")).thenReturn(Optional.of(tag)); + + // Act and Assert + assertThrows(BadRequestException.class, () -> groupService.addGroupTag(requestDto)); + verify(groupRepository, never()).save(any()); + } + + @Test + void testAddGroupTagTagAlreadyAdded() { + // Arrange + AddGroupTagRequestDto requestDto = new AddGroupTagRequestDto(); + requestDto.setGroupId("groupId"); + requestDto.setTagId("tagId"); + Tag grpTag = new Tag(); + grpTag.setId("tagId"); + Group group = new Group(); + group.setId("groupId"); + group.addTag(grpTag); + + Tag tag = new Tag(); + tag.setId("tagId"); + tag.setTagType(TagType.GROUP); + + when(groupRepository.findById("groupId")).thenReturn(Optional.of(group)); + when(tagRepository.findById("tagId")).thenReturn(Optional.of(tag)); + + // Act and Assert + assertThrows(BadRequestException.class, () -> groupService.addGroupTag(requestDto)); + verify(groupRepository, never()).save(any()); + } + @Test + void testRemoveGroupTag() { + // Arrange + RemoveGroupTagRequestDto requestDto = new RemoveGroupTagRequestDto(); + requestDto.setGroupId("groupId"); + requestDto.setTagId("tagId"); + + Group group = new Group(); + group.setId("groupId"); + Tag grpTag = new Tag(); + grpTag.setId("tagId"); + group.addTag(grpTag); + + Tag tag = new Tag(); + tag.setId("tagId"); + + when(groupRepository.findById("groupId")).thenReturn(Optional.of(group)); + when(tagRepository.findById("tagId")).thenReturn(Optional.of(tag)); + + // Act + boolean result = groupService.removeGroupTag(requestDto); + + // Assert + assertTrue(result); + assertFalse(group.getTags().contains("tagId")); + verify(groupRepository, times(1)).save(group); + } + + @Test + void testAddModerator() { + // Arrange + String groupId = "groupId"; + String userId = "userId"; + + Group group = new Group(); + group.setId(groupId); + group.addModerator("moderatorId"); + + User user = new User(); + user.setId("moderatorId"); + + when(groupRepository.findById(groupId)).thenReturn(Optional.of(group)); + + // Act + boolean result = groupService.addModerator(groupId, userId, user); + + // Assert + assertTrue(result); + assertTrue(group.getModerators().contains(userId)); + verify(groupRepository, times(1)).save(group); + } + + @Test + void testAddModeratorNotAllowed() { + // Arrange + String groupId = "groupId"; + String userId = "userId"; + + Group group = new Group(); + group.setId(groupId); + group.addModerator("otherModeratorId"); + + User user = new User(); + user.setId("nonModerator"); + + when(groupRepository.findById(groupId)).thenReturn(Optional.of(group)); + + // Act and Assert + assertThrows(BadRequestException.class, () -> groupService.addModerator(groupId, userId, user)); + verify(groupRepository, never()).save(any()); + } + @Test + void testRemoveModeratorNotFound() { + String groupId = "groupId"; + String userId = "userId"; + String notGroupId = "groupId2"; + + Group group = new Group(); + group.setId(groupId); + group.addModerator(userId); + + User user = new User(); + user.setId(userId); + + when(groupRepository.findById(groupId)).thenReturn(Optional.of(group)); + + assertThrows(ResourceNotFoundException.class, () -> groupService.removeModerator(notGroupId, userId)); + verify(groupRepository, never()).save(any()); + } + + @Test + void testRemoveModerator() { + // Arrange + String groupId = "groupId"; + String moderatorId = "moderatorId"; + + Group group = new Group(); + group.setId(groupId); + group.addModerator(moderatorId); + + when(groupRepository.findById(groupId)).thenReturn(Optional.of(group)); + + boolean result = groupService.removeModerator(groupId, moderatorId); + + assertTrue(result); + assertFalse(group.getModerators().contains(moderatorId)); + verify(groupRepository, times(1)).save(group); + } + + @Test + void testReviewApplicationNotFound() { + String applicationId = "nonexistent"; + User user = new User(); + user.setId("moderator1"); + + GroupApplicationReviewDto reviewDto = new GroupApplicationReviewDto(); + reviewDto.setResult(GroupApplicationReviewResult.APPROVE.name()); + + when(groupApplicationRepository.findById(applicationId)).thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, () -> groupService.reviewApplication(applicationId, user, reviewDto)); + verify(groupRepository, never()).save(any()); + } + + + +} + diff --git a/app/backend/src/test/java/com/app/gamereview/service/NotificationServiceTest.java b/app/backend/src/test/java/com/app/gamereview/service/NotificationServiceTest.java new file mode 100644 index 00000000..041db566 --- /dev/null +++ b/app/backend/src/test/java/com/app/gamereview/service/NotificationServiceTest.java @@ -0,0 +1,106 @@ +package com.app.gamereview.service; + +import com.app.gamereview.dto.request.notification.CreateNotificationRequestDto; +import com.app.gamereview.dto.request.notification.GetNotificationsRequestDto; +import com.app.gamereview.model.Notification; +import com.app.gamereview.repository.NotificationRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.modelmapper.ModelMapper; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +public class NotificationServiceTest { + + @Mock + private NotificationRepository notificationRepository; + + @Mock + private MongoTemplate mongoTemplate; + + @Mock + private ModelMapper modelMapper; + + @InjectMocks + private NotificationService notificationService; + + private Notification notification; + + @BeforeEach + public void setup() { + notification = new Notification(); + notification.setId("123"); + notification.setUser("user1"); + notification.setIsRead(false); + notification.setCreatedAt(LocalDateTime.now()); + } + + @Test + public void testGetNotificationList() { + when(mongoTemplate.find(any(Query.class), eq(Notification.class))).thenReturn(Arrays.asList(notification)); + + GetNotificationsRequestDto requestDto = new GetNotificationsRequestDto(); + requestDto.setIsRead(false); + + List result = notificationService.getNotificationList("user1", requestDto); + + assertFalse(result.isEmpty()); + verify(mongoTemplate, times(1)).find(any(Query.class), eq(Notification.class)); + } + + @Test + public void testCreateNotification() { + CreateNotificationRequestDto requestDto = new CreateNotificationRequestDto(); + requestDto.setUser("user1"); + + when(modelMapper.map(requestDto, Notification.class)).thenReturn(notification); + when(notificationRepository.save(any(Notification.class))).thenReturn(notification); + + Notification result = notificationService.createNotification(requestDto); + + assertNotNull(result); + assertEquals("user1", result.getUser()); + assertFalse(result.getIsRead()); + verify(notificationRepository, times(1)).save(any(Notification.class)); + } + + @Test + public void testGetNotificationListWithNullFilter() { + // Setup notifications + Notification unreadNotification = new Notification(); + unreadNotification.setId("notif1"); + unreadNotification.setUser("user1"); + unreadNotification.setIsRead(false); + + Notification readNotification = new Notification(); + readNotification.setId("notif2"); + readNotification.setUser("user1"); + readNotification.setIsRead(true); + + // Setup the returned list from mongoTemplate + when(mongoTemplate.find(any(Query.class), eq(Notification.class))) + .thenReturn(Arrays.asList(unreadNotification, readNotification)); + + // Call the method with null filter + List result = notificationService.getNotificationList("user1", null); + + // Assertions and Verifications + assertFalse(result.isEmpty()); + assertEquals(2, result.size()); + verify(notificationRepository, times(1)).save(unreadNotification); // Only the unread notification should be saved + verify(notificationRepository, never()).save(readNotification); // Read notification should not be saved + } +} \ No newline at end of file diff --git a/app/backend/src/test/java/com/app/gamereview/service/PostServiceTest.java b/app/backend/src/test/java/com/app/gamereview/service/PostServiceTest.java new file mode 100644 index 00000000..2ba2b25b --- /dev/null +++ b/app/backend/src/test/java/com/app/gamereview/service/PostServiceTest.java @@ -0,0 +1,588 @@ +package com.app.gamereview.service; + +import com.app.gamereview.dto.request.home.HomePagePostsFilterRequestDto; +import com.app.gamereview.dto.request.notification.CreateNotificationRequestDto; +import com.app.gamereview.dto.request.post.CreatePostRequestDto; +import com.app.gamereview.dto.request.post.EditPostRequestDto; +import com.app.gamereview.dto.request.post.GetPostListFilterRequestDto; +import com.app.gamereview.dto.response.comment.GetPostCommentsResponseDto; +import com.app.gamereview.dto.response.home.HomePagePostResponseDto; +import com.app.gamereview.dto.response.post.GetPostDetailResponseDto; +import com.app.gamereview.dto.response.post.GetPostListResponseDto; +import com.app.gamereview.enums.ForumType; +import com.app.gamereview.enums.SortDirection; +import com.app.gamereview.enums.SortType; +import com.app.gamereview.enums.VoteChoice; +import com.app.gamereview.exception.ResourceNotFoundException; +import com.app.gamereview.model.*; +import com.app.gamereview.repository.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.modelmapper.ModelMapper; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.bson.assertions.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +public class PostServiceTest { + + @Mock + private PostRepository postRepository; + + @Mock + private ForumRepository forumRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private ProfileRepository profileRepository; + + @Mock + private TagRepository tagRepository; + + @Mock + private CommentRepository commentRepository; + + @Mock + private GameRepository gameRepository; + + @Mock + private GroupRepository groupRepository; + + @Mock + private NotificationRepository notificationRepository; + + @Mock + private VoteRepository voteRepository; + + @Mock + private AchievementRepository achievementRepository; + + @Mock + private CharacterRepository characterRepository; + + @Mock + private MongoTemplate mongoTemplate; + + @Mock + private ModelMapper modelMapper; + + @InjectMocks + private PostService postService; + @Mock + private NotificationService notificationService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testGetPostList_Success() { + // Arrange + GetPostListFilterRequestDto filter = new GetPostListFilterRequestDto(); + String email = "user@example.com"; + User loggedInUser = new User(); + loggedInUser.setId("userId"); + loggedInUser.setEmail(email); + + when(userRepository.findByEmailAndIsDeletedFalse(anyString())).thenReturn(Optional.of(loggedInUser)); + when(mongoTemplate.find(any(Query.class), eq(Post.class))).thenReturn(Collections.emptyList()); + + // Act + List result = postService.getPostList(filter, email); + + // Assert + Assertions.assertNotNull(result); + verify(mongoTemplate, times(1)).find(any(Query.class), eq(Post.class)); + } + + @Test + void testGetPostById_Success() { + // Arrange + String postId = "postId"; + String email = "user@example.com"; + User loggedInUser = new User(); + loggedInUser.setId("userId"); + loggedInUser.setEmail(email); + Post post = new Post(); + ArrayList tagList = new ArrayList<>(); + tagList.add("tag"); + post.setTags(tagList); + post.setAchievement(""); + post.setCharacter(""); + + when(postRepository.findById(anyString())).thenReturn(Optional.of(post)); + when(userRepository.findByEmailAndIsDeletedFalse(anyString())).thenReturn(Optional.of(loggedInUser)); + when(forumRepository.findById(anyString())).thenReturn(Optional.of(new Forum())); + when(voteRepository.findByTypeIdAndVotedBy(anyString(), anyString())).thenReturn(Optional.empty()); + when(achievementRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.empty()); + when(characterRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.empty()); + when(tagRepository.findById(anyString())).thenReturn(Optional.empty()); + when(modelMapper.map(any(Optional.class), eq(GetPostDetailResponseDto.class))).thenReturn(new GetPostDetailResponseDto()); + + // Act + GetPostDetailResponseDto result = postService.getPostById(postId, email); + + // Assert + Assertions.assertNotNull(result); + verify(voteRepository, times(1)).findByTypeIdAndVotedBy(anyString(), anyString()); + verify(achievementRepository, times(1)).findByIdAndIsDeletedFalse(anyString()); + verify(characterRepository, times(1)).findByIdAndIsDeletedFalse(anyString()); + verify(tagRepository, times(1)).findById(anyString()); + } + + @Test + void testGetPostById_NotFound() { + // Arrange + String postId = "nonExistentPostId"; + String email = "user@example.com"; + User loggedInUser = new User(); + loggedInUser.setId("userId"); + loggedInUser.setEmail(email); + + when(postRepository.findById(anyString())).thenReturn(Optional.empty()); + when(userRepository.findByEmailAndIsDeletedFalse(anyString())).thenReturn(Optional.of(loggedInUser)); + + // Act and Assert + assertThrows(ResourceNotFoundException.class, + () -> postService.getPostById(postId, email), + "Should throw ResourceNotFoundException when post is not found"); + + // Verify + verify(postRepository, times(1)).findById(anyString()); + verify(userRepository, times(1)).findByEmailAndIsDeletedFalse(anyString()); + } + + @Test + void testCreatePost_Success() { + // Arrange + CreatePostRequestDto request = new CreatePostRequestDto(); + Forum forum = new Forum(); + forum.setId("forumId"); + forum.setName("forumName"); + request.setForum("forumId"); + request.setTags(Collections.singletonList("tagId")); + User user = new User(); + user.setId("userId"); + user.setUsername("username"); + Profile profile = new Profile(); + profile.setUserId(user.getId()); + + Post post = new Post(); + + Notification notification = new Notification(); + + when(forumRepository.findById(anyString())).thenReturn(Optional.of(forum)); + when(achievementRepository.findById(anyString())).thenReturn(Optional.of(new Achievement())); + when(tagRepository.findById(anyString())).thenReturn(Optional.of(new Tag())); + when(userRepository.findByEmailAndIsDeletedFalse(anyString())).thenReturn(Optional.of(user)); + when(profileRepository.save(any(Profile.class))).thenReturn(profile); + when(postRepository.save(any(Post.class))).thenReturn(post); + when(notificationService.createNotification(any(CreateNotificationRequestDto.class))).thenReturn(notification); + when(mongoTemplate.findOne(any(Query.class), eq(Profile.class))).thenReturn(profile); + when(modelMapper.map(any(CreatePostRequestDto.class), eq(Post.class))).thenReturn(post); + // Act + Post result = postService.createPost(request, user); + + // Assert + Assertions.assertNotNull(result); + verify(profileRepository, times(1)).save(any(Profile.class)); + verify(postRepository, times(1)).save(any(Post.class)); + } + + @Test + void testEditPost_Success() { + // Arrange + String postId = "postId"; + EditPostRequestDto request = new EditPostRequestDto(); + request.setTitle("New Title"); + User user = new User(); + user.setId("userId"); + Post post = new Post(); + post.setId(postId); + post.setPoster(user.getId()); + post.setTitle("old title"); + + when(postRepository.findById(anyString())).thenReturn(Optional.of(post)); + when(userRepository.findByEmailAndIsDeletedFalse(anyString())).thenReturn(Optional.of(user)); + when(postRepository.save(any(Post.class))).thenReturn(post); + + // Act + Post result = postService.editPost(postId, request, user); + + // Assert + Assertions.assertNotNull(result); + verify(postRepository, times(1)).save(any(Post.class)); + } + + @Test + void testDeletePost_Success() { + // Arrange + String postId = "postId"; + User user = new User(); + user.setId("userId"); + Post existingPost = new Post(); + existingPost.setId(postId); + existingPost.setPoster(user.getId()); + + when(postRepository.findById(anyString())).thenReturn(Optional.of(existingPost)); + when(userRepository.findByEmailAndIsDeletedFalse(anyString())).thenReturn(Optional.of(user)); + when(mongoTemplate.findOne(any(Query.class), eq(Profile.class))).thenReturn(new Profile()); + when(profileRepository.save(any(Profile.class))).thenReturn(new Profile()); + when(postRepository.save(any(Post.class))).thenReturn(existingPost); + + // Act + Post result = postService.deletePost(postId, user); + + // Assert + Assertions.assertNotNull(result); + verify(profileRepository, times(1)).save(any(Profile.class)); + verify(postRepository, times(1)).save(any(Post.class)); + } + + @Test + void testGetUserPostList_Success() { + // Arrange + User user = new User(); + user.setId("userId"); + + when(mongoTemplate.find(any(Query.class), eq(Post.class))).thenReturn(Collections.emptyList()); + + // Act + List result = postService.getUserPostList(user); + + // Assert + Assertions.assertNotNull(result); + verify(mongoTemplate, times(1)).find(any(Query.class), eq(Post.class)); + } + + @Test + void testGetCommentList_Success() { + // Arrange + String postId = "postId"; + String userId = "userId"; + User user = new User(); + user.setId(userId); + + // Mock data + Post post = new Post(); + post.setId(postId); + post.setForum("forumId"); + + Forum forum = new Forum(); + forum.setId("forumId"); + forum.setBannedUsers(List.of("")); + + Comment comment1 = new Comment(); + comment1.setId("commentId1"); + comment1.setCreatedAt(LocalDateTime.now()); + comment1.setCommenter("userId"); + comment1.setParentComment(null); + comment1.setCommentContent(""); + comment1.setPost(postId); + comment1.setLastEditedAt(LocalDateTime.now()); + comment1.setIsDeleted(false); + comment1.setOverallVote(5); + comment1.setVoteCount(10); + + + Comment comment2 = new Comment(); + comment2.setId("commentId2"); + comment2.setCreatedAt(LocalDateTime.now()); + comment2.setCommenter("userId"); + comment2.setCommentContent(""); + comment2.setPost(postId); + comment2.setLastEditedAt(LocalDateTime.now()); + comment2.setIsDeleted(false); + comment2.setOverallVote(5); + comment2.setVoteCount(10); + comment2.setParentComment("commentId1"); + + Vote vote = new Vote(); + vote.setId("voteId"); + vote.setChoice(VoteChoice.UPVOTE); + + List comments = List.of(comment1, comment2); + + when(postRepository.findById(anyString())).thenReturn(Optional.of(post)); + when(forumRepository.findById(anyString())).thenReturn(Optional.of(forum)); + when(commentRepository.findByPost(anyString())).thenReturn(comments); + when(userRepository.findByIdAndIsDeletedFalse(any(String.class))).thenReturn(Optional.of(user)); + when(voteRepository.findByTypeIdAndVotedBy(any(String.class), any(String.class))).thenReturn(Optional.of(vote)); + + List result = postService.getCommentList(postId, userId); + + assertNotNull(result); + assertEquals(1, result.size()); // Assuming only one top-level comment in the test data + assertEquals(comment1.getId(), result.get(0).getId()); + assertEquals(1, result.get(0).getReplies().size()); // Assuming one reply in the test data + assertEquals(comment2.getId(), result.get(0).getReplies().get(0).getId()); + + verify(postRepository, times(1)).findById(anyString()); + verify(forumRepository, times(1)).findById(anyString()); + verify(commentRepository, times(1)).findByPost(anyString()); + } + + @Test + void testGetHomePagePostsOfUser_Success() { + HomePagePostsFilterRequestDto filter = new HomePagePostsFilterRequestDto(); + User user = new User(); + user.setId("userId"); + + Profile profile = new Profile(); + profile.setUserId(user.getId()); + profile.setGames(Collections.singletonList("gameId")); + + Game game = new Game(); + game.setId("gameId"); + game.setForum("gameForumId"); + + Group group = new Group(); + group.setId("groupId"); + group.setForumId("groupForumId"); + + Forum forum = new Forum(); + forum.setId("forumId"); + forum.setType(ForumType.GAME); + forum.setParent("gameId"); + + Post post = new Post(); + post.setId("postId"); + post.setForum("gameForumId"); + post.setTags(List.of("tagId")); + + Post extraPost = new Post(); + extraPost.setId("extraPostId"); + extraPost.setForum("extraGameForumId"); + extraPost.setTags(List.of("tagId")); + + Tag randomTag = new Tag(); + randomTag.setId("tagId"); + + HomePagePostResponseDto responseDto = new HomePagePostResponseDto(); + responseDto.setTags(List.of(randomTag)); + + when(profileRepository.findByUserIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(profile)); + when(gameRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(game)); + when(groupRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(group)); + when(postRepository.findByForumAndIsDeletedFalse(anyString())).thenReturn(Collections.singletonList(post)); + when(forumRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(forum)); + when(mongoTemplate.find(any(Query.class), eq(Group.class))).thenReturn(Collections.singletonList(group)); + when(mongoTemplate.find(any(Query.class), eq(Post.class))).thenReturn(Collections.singletonList(extraPost)); + when(modelMapper.map(any(Post.class), eq(HomePagePostResponseDto.class))).thenReturn(responseDto); + + List result = postService.getHomePagePostsOfUser(filter, user); + + assertNotNull(result); + assertEquals(3, result.size()); + } + + @Test + void testGetHomePagePostsOfUser_Success_Group_Forum() { + HomePagePostsFilterRequestDto filter = new HomePagePostsFilterRequestDto(); + filter.setSortBy(SortType.OVERALL_VOTE.name()); + filter.setSortDirection(SortDirection.ASCENDING.name()); + + User user = new User(); + user.setId("userId"); + + Profile profile = new Profile(); + profile.setUserId(user.getId()); + profile.setGames(Collections.singletonList("gameId")); + + Game game = new Game(); + game.setId("gameId"); + game.setForum("gameForumId"); + + Group group = new Group(); + group.setId("groupId"); + group.setForumId("groupForumId"); + + Forum forum = new Forum(); + forum.setId("forumId"); + forum.setType(ForumType.GAME); + forum.setParent("gameId"); + + Post post = new Post(); + post.setId("postId"); + post.setForum("gameForumId"); + post.setTags(List.of("tagId")); + + Post extraPost = new Post(); + extraPost.setId("extraPostId"); + extraPost.setForum("extraGameForumId"); + extraPost.setTags(List.of("tagId")); + + Tag randomTag = new Tag(); + randomTag.setId("tagId"); + + HomePagePostResponseDto responseDto = new HomePagePostResponseDto(); + responseDto.setTags(List.of(randomTag)); + + when(profileRepository.findByUserIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(profile)); + when(gameRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(game)); + when(groupRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(group)); + when(postRepository.findByForumAndIsDeletedFalse(anyString())).thenReturn(Collections.singletonList(post)); + when(forumRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(forum)); + when(mongoTemplate.find(any(Query.class), eq(Group.class))).thenReturn(Collections.singletonList(group)); + when(mongoTemplate.find(any(Query.class), eq(Post.class))).thenReturn(Collections.singletonList(extraPost)); + when(modelMapper.map(any(Post.class), eq(HomePagePostResponseDto.class))).thenReturn(responseDto); + + List result = postService.getHomePagePostsOfUser(filter, user); + + assertNotNull(result); + assertEquals(3, result.size()); + } + + @Test + void testGetHomePagePostsOfGuest_Success() { + HomePagePostsFilterRequestDto filter = new HomePagePostsFilterRequestDto(); + User user = new User(); + user.setId("userId"); + + Profile profile = new Profile(); + profile.setUserId(user.getId()); + profile.setGames(Collections.singletonList("gameId")); + + Game game = new Game(); + game.setId("gameId"); + game.setForum("gameForumId"); + + Group group = new Group(); + group.setId("groupId"); + group.setForumId("groupForumId"); + + Forum forum = new Forum(); + forum.setId("forumId"); + forum.setType(ForumType.GAME); + forum.setParent("gameId"); + + Post post = new Post(); + post.setId("postId"); + post.setForum("gameForumId"); + post.setTags(List.of("tagId")); + + Post extraPost = new Post(); + extraPost.setId("extraPostId"); + extraPost.setForum("extraGameForumId"); + extraPost.setTags(List.of("tagId")); + + Tag randomTag = new Tag(); + randomTag.setId("tagId"); + + HomePagePostResponseDto responseDto = new HomePagePostResponseDto(); + responseDto.setTags(List.of(randomTag)); + + when(profileRepository.findByUserIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(profile)); + when(gameRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(game)); + when(groupRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(group)); + when(postRepository.findByForumAndIsDeletedFalse(anyString())).thenReturn(Collections.singletonList(post)); + when(forumRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(forum)); + when(mongoTemplate.find(any(Query.class), eq(Group.class))).thenReturn(Collections.singletonList(group)); + when(mongoTemplate.find(any(Query.class), eq(Post.class))).thenReturn(Collections.singletonList(extraPost)); + when(mongoTemplate.find(any(Query.class), eq(Forum.class))).thenReturn(Collections.singletonList(forum)); + when(modelMapper.map(any(Post.class), eq(HomePagePostResponseDto.class))).thenReturn(responseDto); + + List result = postService.getHomePagePostsOfGuest(filter); + + assertNotNull(result); + assertEquals(1, result.size()); + } + + @Test + void testGetHomePagePostsOfGuest_Success_Group_Forum() { + HomePagePostsFilterRequestDto filter = new HomePagePostsFilterRequestDto(); + filter.setSortBy(SortType.OVERALL_VOTE.name()); + filter.setSortDirection(SortDirection.ASCENDING.name()); + + User user = new User(); + user.setId("userId"); + + Profile profile = new Profile(); + profile.setUserId(user.getId()); + profile.setGames(Collections.singletonList("gameId")); + + Game game = new Game(); + game.setId("gameId"); + game.setForum("gameForumId"); + + Group group = new Group(); + group.setId("groupId"); + group.setForumId("groupForumId"); + + Forum forum = new Forum(); + forum.setId("forumId"); + forum.setType(ForumType.GROUP); + forum.setParent("gameId"); + + Post post = new Post(); + post.setId("postId"); + post.setForum("gameForumId"); + post.setTags(List.of("tagId")); + + Post extraPost = new Post(); + extraPost.setId("extraPostId"); + extraPost.setForum("extraGameForumId"); + extraPost.setTags(List.of("tagId")); + + Tag randomTag = new Tag(); + randomTag.setId("tagId"); + + HomePagePostResponseDto responseDto = new HomePagePostResponseDto(); + responseDto.setTags(List.of(randomTag)); + + when(profileRepository.findByUserIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(profile)); + when(gameRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(game)); + when(groupRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(group)); + when(postRepository.findByForumAndIsDeletedFalse(anyString())).thenReturn(Collections.singletonList(post)); + when(forumRepository.findByIdAndIsDeletedFalse(anyString())).thenReturn(Optional.of(forum)); + when(mongoTemplate.find(any(Query.class), eq(Group.class))).thenReturn(Collections.singletonList(group)); + when(mongoTemplate.find(any(Query.class), eq(Post.class))).thenReturn(Collections.singletonList(extraPost)); + when(mongoTemplate.find(any(Query.class), eq(Forum.class))).thenReturn(Collections.singletonList(forum)); + when(modelMapper.map(any(Post.class), eq(HomePagePostResponseDto.class))).thenReturn(responseDto); + + List result = postService.getHomePagePostsOfGuest(filter); + + assertNotNull(result); + assertEquals(1, result.size()); + } + + @Test + void testGetHomepagePosts_UserNotFound() { + HomePagePostsFilterRequestDto filter = new HomePagePostsFilterRequestDto(); + String email = "nonexistentuser@example.com"; + + when(userRepository.findByEmailAndIsDeletedFalse(anyString())).thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, + () -> postService.getHomepagePosts(filter, email), + "Should throw ResourceNotFoundException when user is not found"); + + verify(userRepository, times(1)).findByEmailAndIsDeletedFalse(anyString()); + } + + @Test + void testGetHomepagePosts_Guest() { + HomePagePostsFilterRequestDto filter = new HomePagePostsFilterRequestDto(); + + List result = postService.getHomepagePosts(filter, null); + + verifyNoInteractions(userRepository); + } +} diff --git a/app/backend/src/test/java/com/app/gamereview/service/ProfileServiceTests.java b/app/backend/src/test/java/com/app/gamereview/service/ProfileServiceTests.java new file mode 100644 index 00000000..14cfe13e --- /dev/null +++ b/app/backend/src/test/java/com/app/gamereview/service/ProfileServiceTests.java @@ -0,0 +1,283 @@ +package com.app.gamereview.service; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.app.gamereview.dto.request.review.GetAllReviewsFilterRequestDto; +import com.app.gamereview.dto.request.vote.GetAllVotesFilterRequestDto; +import com.app.gamereview.dto.response.profile.GetLastActivitiesResponseDto; +import com.app.gamereview.dto.response.review.GetAllReviewsResponseDto; +import com.app.gamereview.enums.ActivityType; +import com.app.gamereview.enums.UserRole; +import com.app.gamereview.enums.VoteChoice; +import com.app.gamereview.enums.VoteType; +import com.app.gamereview.model.*; +import com.app.gamereview.repository.*; +import com.app.gamereview.dto.request.profile.EditProfileRequestDto; +import com.app.gamereview.dto.request.profile.ProfileUpdateGameRequestDto; +import com.app.gamereview.exception.ResourceNotFoundException; +import com.app.gamereview.exception.BadRequestException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public class ProfileServiceTests { + + @Mock + private ProfileRepository profileRepository; + + @Mock + private GameRepository gameRepository; + + @Mock + private ReviewRepository reviewRepository; + @Mock + private GroupRepository groupRepository; + + @Mock + private UserRepository userRepository; + @Mock + private ReviewService reviewService; + @Mock + private VoteService voteService; + + @Mock + private CommentService commentService; + @Mock + private PostService postService; + + @InjectMocks + private ProfileService profileService; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testEditProfileSuccess() { + String profileId = "profileId"; + String userId = "user1"; + Profile mockProfile = new Profile(); + mockProfile.setUserId(userId); + + when(profileRepository.findById(profileId)).thenReturn(Optional.of(mockProfile)); + when(profileRepository.save(any(Profile.class))).thenReturn(mockProfile); + + User mockUser = new User(); + mockUser.setId(userId); + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(userRepository.save(any(User.class))).thenReturn(mockUser); + + EditProfileRequestDto request = new EditProfileRequestDto(); + Profile result = profileService.editProfile(profileId, request, mockUser); + + assertNotNull(result, "The result should not be null"); + assertEquals(mockProfile, result, "The returned profile should match the mock profile"); + } + + @Test + public void testEditProfileNotFound() { + when(profileRepository.findById(anyString())).thenReturn(Optional.empty()); + + EditProfileRequestDto request = new EditProfileRequestDto(); + User user = new User(); + + // Assert that ResourceNotFoundException is thrown + assertThrows(ResourceNotFoundException.class, () -> { + profileService.editProfile("invalidId", request, user); + }); + } + + @Test + public void testAddGameToProfile() { + String profileId = "profileId"; + String gameId = "gameId"; + String userId = "user1"; + + User mockUser = new User(); + mockUser.setId(userId); + mockUser.setRole(UserRole.ADMIN); // or USER, if testing for user ownership + + Profile mockProfile = new Profile(); + mockProfile.setUserId(userId); + mockProfile.setIsDeleted(false); // Ensure profile is not marked as deleted + when(profileRepository.findById(profileId)).thenReturn(Optional.of(mockProfile)); + + Game mockGame = new Game(); + mockGame.setGameName("Test Game"); + mockGame.setIsDeleted(false); // Ensure game is not marked as deleted + when(gameRepository.findById(gameId)).thenReturn(Optional.of(mockGame)); + + ProfileUpdateGameRequestDto request = new ProfileUpdateGameRequestDto(); + request.setGame(gameId); + + profileService.addGameToProfile(profileId, request, mockUser); + + assertTrue(mockProfile.getGames().contains(gameId), "The game should be added to the profile"); + + verify(profileRepository, times(1)).save(mockProfile); + } + + @Test + public void testRemoveGameFromProfile() { + String profileId = "profileId"; + String gameId = "gameId"; + String userId = "userId"; + User mockUser = new User(); + mockUser.setId(userId); + + Profile mockProfile = new Profile(); + mockProfile.setUserId(userId); + mockProfile.addGame(gameId); + when(profileRepository.findById(profileId)).thenReturn(Optional.of(mockProfile)); + + ProfileUpdateGameRequestDto request = new ProfileUpdateGameRequestDto(); + request.setGame(gameId); + + Profile result = profileService.removeGameFromProfile(profileId, request, mockUser); + + assertFalse(mockProfile.getGames().contains(gameId), "The game should be removed from the profile"); + + verify(profileRepository, times(1)).save(mockProfile); + } + + + @Test + public void testGetProfilePrivateProfileAccessDenied() { + String profileUserId = "ownerId"; + String nonOwnerUserId = "nonOwnerId"; + + Profile mockProfile = new Profile(); + mockProfile.setUserId(profileUserId); + mockProfile.setIsPrivate(true); + + User nonOwnerUser = new User(); + nonOwnerUser.setId(nonOwnerUserId); + nonOwnerUser.setRole(UserRole.BASIC); + + when(profileRepository.findByUserIdAndIsDeletedFalse(profileUserId)).thenReturn(Optional.of(mockProfile)); + + assertThrows(BadRequestException.class, () -> { + profileService.getProfile(profileUserId, nonOwnerUser.getEmail()); + }, "Expected BadRequestException for accessing a private profile by a non-owner and non-admin user"); + } + @Test + public void testEditProfileUsernameAlreadyTaken() { + String profileId = "profileId"; + String userId = "user1"; + String existingUsername = "existingUsername"; + String newUsername = "newUsername"; + + Profile mockProfile = new Profile(); + mockProfile.setUserId(userId); + + when(profileRepository.findById(profileId)).thenReturn(Optional.of(mockProfile)); + + User existingUser = new User(); + existingUser.setId("existingUserId"); + existingUser.setUsername(newUsername); + when(userRepository.findByUsername(newUsername)).thenReturn(Optional.of(existingUser)); + + User mockUser = new User(); + mockUser.setId(userId); + mockUser.setUsername(existingUsername); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + + EditProfileRequestDto request = new EditProfileRequestDto(); + request.setUsername(newUsername); + + assertThrows(BadRequestException.class, () -> { + profileService.editProfile(profileId, request, mockUser); + }, "Expected BadRequestException for attempting to use an already taken username"); + } + + + @Test + public void testGetProfileSuccess() { + String userId = "userId"; + String email = "user@example.com"; + + Profile mockProfile = new Profile(); + mockProfile.setUserId(userId); + mockProfile.setIsPrivate(false); + + User mockUser = new User(); + mockUser.setId(userId); + mockUser.setEmail(email); + + when(profileRepository.findByUserIdAndIsDeletedFalse(userId)).thenReturn(Optional.of(mockProfile)); + when(userRepository.findByEmailAndIsDeletedFalse(email)).thenReturn(Optional.of(mockUser)); + + profileService.getProfile(userId, email); + + // Assertions + assertEquals(userId, mockProfile.getUserId(), "The profile should match the requested user id"); + assertFalse(mockProfile.getIsPrivate(), "The profile should be public"); + } + @Test + public void testGetLastActivities() { + User user = new User(); + user.setId("userId"); + user.setEmail("user@example.com"); + + GetAllReviewsFilterRequestDto reviewFilter = new GetAllReviewsFilterRequestDto(); + reviewFilter.setReviewedBy(user.getId()); + reviewFilter.setSortBy("CREATION_DATE"); + + GetAllVotesFilterRequestDto voteFilter = new GetAllVotesFilterRequestDto(); + voteFilter.setVotedBy(user.getId()); + + GetAllReviewsResponseDto mockReview = new GetAllReviewsResponseDto(); + mockReview.setId("reviewId"); + mockReview.setGameId("gameId"); + mockReview.setReviewDescription("This is a review"); + mockReview.setCreatedAt(LocalDateTime.parse("2023-01-01T12:00:00")); + + Vote mockVote = new Vote(); + mockVote.setId("voteId"); + mockVote.setTypeId("reviewId"); + mockVote.setVoteType(VoteType.REVIEW); + mockVote.setChoice(VoteChoice.UPVOTE); + mockVote.setVotedBy(user.getId()); + mockVote.setCreatedAt(LocalDateTime.parse("2023-01-02T12:00:00")); + + Comment mockComment = new Comment(); + mockComment.setId("commentId"); + mockComment.setPost("postId"); + mockComment.setCommentContent("This is a comment"); + mockComment.setCreatedAt(LocalDateTime.parse("2023-01-04T12:00:00")); + + // Mock post data + Post mockPost = new Post(); + mockPost.setId("postId"); + mockPost.setForum("forumId"); + mockPost.setPostContent("This is a post"); + mockPost.setCreatedAt(LocalDateTime.parse("2023-01-05T12:00:00")); + + when(reviewService.getAllReviews(any(), any())).thenReturn(List.of(mockReview)); + when(voteService.getAllVotes(any())).thenReturn(List.of(mockVote)); + when(commentService.getUserCommentList(any())).thenReturn(List.of(mockComment)); + when(postService.getUserPostList(any())).thenReturn(List.of(mockPost)); + + List lastActivities = profileService.getLastActivities(user); + System.out.println(lastActivities); + assertNotNull(lastActivities, "The result should not be null"); + assertEquals(4, lastActivities.size(), "There should be four activity"); + for (int i = 1; i < lastActivities.size(); i++) { + GetLastActivitiesResponseDto currentActivity = lastActivities.get(i); + GetLastActivitiesResponseDto previousActivity = lastActivities.get(i - 1); + + // Check that the current activity's creation date is equal to or after the previous one + assertFalse(currentActivity.getCreatedAt().isAfter(previousActivity.getCreatedAt()), "Activities should be sorted in descending order based on creation date"); + } + } +} diff --git a/app/backend/src/test/java/com/app/gamereview/service/ReviewServiceTest.java b/app/backend/src/test/java/com/app/gamereview/service/ReviewServiceTest.java new file mode 100644 index 00000000..fed30ce7 --- /dev/null +++ b/app/backend/src/test/java/com/app/gamereview/service/ReviewServiceTest.java @@ -0,0 +1,269 @@ +package com.app.gamereview.service; + +import com.app.gamereview.dto.request.review.CreateReviewRequestDto; +import com.app.gamereview.dto.request.review.GetAllReviewsFilterRequestDto; +import com.app.gamereview.dto.request.review.UpdateReviewRequestDto; +import com.app.gamereview.dto.response.review.GetAllReviewsResponseDto; +import com.app.gamereview.enums.SortDirection; +import com.app.gamereview.enums.SortType; +import com.app.gamereview.exception.ResourceNotFoundException; +import com.app.gamereview.model.Game; +import com.app.gamereview.model.Profile; +import com.app.gamereview.model.Review; +import com.app.gamereview.model.User; +import com.app.gamereview.repository.*; +import com.mongodb.client.result.UpdateResult; +import org.bson.BsonValue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.modelmapper.ModelMapper; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +public class ReviewServiceTest { + + @Mock + private ReviewRepository reviewRepository; + + @Mock + private GameRepository gameRepository; + + @Mock + private VoteRepository voteRepository; + + @Mock + private AchievementRepository achievementRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private ProfileRepository profileRepository; + + @Mock + private MongoTemplate mongoTemplate; + + @Mock + private ModelMapper modelMapper; + + @InjectMocks + private ReviewService reviewService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testGetAllReviews() { + // Arrange + GetAllReviewsFilterRequestDto filter = new GetAllReviewsFilterRequestDto(); + filter.setGameId("exampleGameId"); + filter.setReviewedBy("exampleUserId"); + filter.setWithDeleted(false); + filter.setSortBy(SortType.CREATION_DATE.name()); + filter.setSortDirection(SortDirection.DESCENDING.name()); + + Review review1 = new Review(); + review1.setId("1"); + review1.setGameId("exampleGameId"); + review1.setReviewedBy("exampleUserId"); + review1.setReviewDescription("Review 1"); + review1.setRating(5); + + Review review2 = new Review(); + review1.setId("2"); + review1.setGameId("exampleGameId"); + review1.setReviewedBy("exampleUserId"); + review1.setReviewDescription("Review 2"); + review1.setRating(4); + + List mockReviews = new ArrayList<>(); + mockReviews.add(review1); + mockReviews.add(review2); + + User user = new User(); + user.setId("exampleUserId"); + user.setEmail("user@example.com"); + + when(userRepository.findByEmailAndIsDeletedFalse(anyString())).thenReturn(Optional.of(user)); + when(mongoTemplate.find(any(Query.class), eq(Review.class))).thenReturn(new ArrayList<>()); + + // Act + List result = reviewService.getAllReviews(filter, "user@example.com"); + + // Assert + assertNotNull(result); + } + + @Test + void testGetReview() { + Review review = new Review(); + review.setId("existingReviewId"); + review.setReviewedBy("userId"); + + User user = new User(); + user.setId("userId"); + user.setUsername("username"); + when(reviewRepository.findById("existingReviewId")).thenReturn(Optional.of(review)); + when(userRepository.findById(review.getReviewedBy())).thenReturn(Optional.of(user)); + + GetAllReviewsResponseDto dto = new GetAllReviewsResponseDto(); + dto.setId(review.getId()); + when(modelMapper.map(any(), any())).thenReturn(dto); + GetAllReviewsResponseDto result = reviewService.getReview("existingReviewId"); + + assertNotNull(result); + assertEquals("existingReviewId", result.getId()); + } + + @Test + void testGetReviewNotFound() { + when(reviewRepository.findById("nonExistingReviewId")).thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, () -> reviewService.getReview("nonExistingReviewId")); + } + + @Test + void testAddReview() { + CreateReviewRequestDto requestDto = new CreateReviewRequestDto(); + + Review review = new Review(); + review.setGameId("gameId"); + review.setRating(5.0f); + Game game = new Game(); + Profile profile = new Profile(); + profile.setIsReviewedYet(true); + + User user = new User(); + user.setId("userId"); + user.setEmail("user@example.com"); + + when(modelMapper.map(any(), any())).thenReturn(review); + when(mongoTemplate.findOne(any(), any())).thenReturn(profile); + when(gameRepository.findById(review.getGameId())).thenReturn(Optional.of(game)); + + Review result = reviewService.addReview(requestDto, user); + + assertNotNull(result); + assertEquals("userId", result.getReviewedBy()); + } + + @Test + void testUpdateReview() { + UpdateReviewRequestDto requestDto = new UpdateReviewRequestDto(); + requestDto.setRating(4.5f); + requestDto.setReviewDescription("New Description"); + + User user = new User(); + user.setId("userId"); + user.setEmail("user@example.com"); + + Review review = new Review(); + review.setId("existingReviewId"); + review.setGameId("gameId"); + review.setReviewedBy("userId"); + review.setReviewDescription("description"); + review.setRating(5.0f); + + Game game = new Game(); + when(reviewRepository.findById("existingReviewId")).thenReturn(Optional.of(review)); + when(gameRepository.findById(review.getGameId())).thenReturn(Optional.of(game)); + + UpdateResult expectedUpdateResult = new UpdateResult() { + @Override + public boolean wasAcknowledged() { + return true; + } + + @Override + public long getMatchedCount() { + return 0; + } + + @Override + public long getModifiedCount() { + return 0; + } + + @Override + public BsonValue getUpsertedId() { + return null; + } + }; + Query query = new Query(Criteria.where("_id").is(review.getId())); + Update update = new Update(); + update.set("reviewDescription", requestDto.getReviewDescription()); + update.set("rating", requestDto.getRating()); + when(mongoTemplate.updateFirst(query,update,Review.class)).thenReturn(expectedUpdateResult); + boolean result = reviewService.updateReview("existingReviewId", requestDto, user); + + assertTrue(result); + } + + @Test + void testUpdateReviewNotFound() { + UpdateReviewRequestDto requestDto = new UpdateReviewRequestDto(); + User user = new User(); + user.setId("userId"); + user.setEmail("user@example.com"); + when(reviewRepository.findById("nonExistingReviewId")).thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, () -> reviewService.updateReview("nonExistingReviewId", requestDto, user)); + } + + @Test + void testDeleteReview() { + User user = new User(); + user.setId("userId"); + user.setEmail("user@example.com"); + + Review review = new Review(); + review.setId("existingReviewId"); + review.setGameId("gameId"); + review.setReviewedBy("userId"); + review.setReviewDescription("description"); + review.setRating(5.0f); + + Profile profile = new Profile(); + profile.setUserId(user.getId()); + + Game game = new Game(); + game.setId(review.getGameId()); + + Query query = new Query(); + query.addCriteria(Criteria.where("userId").is(user.getId())); + when(mongoTemplate.findOne(query, Profile.class)).thenReturn(profile); + when(reviewRepository.findById("existingReviewId")).thenReturn(Optional.of(review)); + when(gameRepository.findById("gameId")).thenReturn(Optional.of(game)); + + boolean result = reviewService.deleteReview("existingReviewId", user); + + assertTrue(result); + } + + @Test + void testDeleteReviewNotFound() { + + User user = new User(); + user.setId("userId"); + user.setEmail("user@example.com"); + + when(reviewRepository.findById("nonExistingReviewId")).thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, () -> reviewService.deleteReview("nonExistingReviewId", user)); + } +} diff --git a/app/backend/src/test/java/com/app/gamereview/service/UserServiceTests.java b/app/backend/src/test/java/com/app/gamereview/service/UserServiceTests.java index 6404c2be..d7f7d3f7 100644 --- a/app/backend/src/test/java/com/app/gamereview/service/UserServiceTests.java +++ b/app/backend/src/test/java/com/app/gamereview/service/UserServiceTests.java @@ -18,7 +18,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -class UserServiceTest { +class UserServiceTests { @Mock private UserRepository userRepository; diff --git a/app/backend/src/test/java/com/app/gamereview/service/VoteServiceTest.java b/app/backend/src/test/java/com/app/gamereview/service/VoteServiceTest.java new file mode 100644 index 00000000..c9af7243 --- /dev/null +++ b/app/backend/src/test/java/com/app/gamereview/service/VoteServiceTest.java @@ -0,0 +1,366 @@ +package com.app.gamereview.service; + +import com.app.gamereview.dto.request.notification.CreateNotificationRequestDto; +import com.app.gamereview.dto.request.vote.*; +import com.app.gamereview.enums.*; +import com.app.gamereview.exception.ResourceNotFoundException; +import com.app.gamereview.model.*; +import com.app.gamereview.repository.*; +import org.junit.jupiter.api.*; +import org.mockito.*; +import org.modelmapper.ModelMapper; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +public class VoteServiceTest { + + @Mock + private VoteRepository voteRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private ReviewRepository reviewRepository; + + @Mock + private NotificationRepository notificationRepository; + + @Mock + private ProfileRepository profileRepository; + + @Mock + private AchievementRepository achievementRepository; + + @Mock + private MongoTemplate mongoTemplate; + + @Mock + private ModelMapper modelMapper; + + @Mock + private PostRepository postRepository; + + @Mock + private CommentRepository commentRepository; + + @Mock NotificationService notificationService; + + @InjectMocks + private VoteService voteService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testGetAllVotes() { + GetAllVotesFilterRequestDto filter = new GetAllVotesFilterRequestDto(); + List mockVotes = Arrays.asList(new Vote(), new Vote()); + when(mongoTemplate.find(any(Query.class), eq(Vote.class))).thenReturn(mockVotes); + + List result = voteService.getAllVotes(filter); + + assertNotNull(result); + assertEquals(2, result.size()); + } + + @Test + void testGetVote() { + Vote existingVote = new Vote(); + existingVote.setId("existingVoteId"); + existingVote.setVotedBy("existingUserId"); + when(voteRepository.findById("existingVoteId")).thenReturn(Optional.of(existingVote)); + + Vote result = voteService.getVote("existingVoteId"); + + assertNotNull(result); + assertEquals("existingVoteId", result.getId()); + } + + @Test + void testAddVote() { + CreateVoteRequestDto requestDto = new CreateVoteRequestDto(); + requestDto.setChoice(VoteChoice.UPVOTE.name()); + requestDto.setTypeId("someReviewId"); + requestDto.setVoteType(VoteType.REVIEW.name()); + + User user = new User(); + user.setId("userId"); + user.setEmail("user@example.com"); + + Profile profile = new Profile(); + profile.setUserId("userId"); + profile.setIsVotedYet(true); + + Vote vote = new Vote(); + vote.setVoteType(VoteType.REVIEW); + vote.setTypeId("someReviewId"); + vote.setChoice(VoteChoice.UPVOTE); + vote.setVotedBy(user.getId()); + + Review review = new Review(); + + when(mongoTemplate.findOne(any(Query.class), eq(Profile.class))).thenReturn(profile); + when(modelMapper.map(any(CreateVoteRequestDto.class), eq(Vote.class))).thenReturn(vote); + when(reviewRepository.findById(any(String.class))).thenReturn(Optional.of(review)); + when(voteRepository.save(vote)).thenReturn(vote); + + Vote result = voteService.addVote(requestDto, user); + + assertNotNull(result); + assertEquals("userId", result.getVotedBy()); + } + + @Test + void testAddReviewVoteFirstTime() { + CreateVoteRequestDto requestDto = new CreateVoteRequestDto(); + requestDto.setChoice(VoteChoice.DOWNVOTE.name()); + requestDto.setTypeId("newReviewId"); + requestDto.setVoteType(VoteType.REVIEW.name()); + + User user = new User(); + user.setId("userId"); + user.setUsername("userName"); + + Profile profile = new Profile(); + profile.setUserId("userId"); + profile.setIsVotedYet(false); + + Vote newVote = new Vote(); + newVote.setId("newVoteId"); + newVote.setChoice(VoteChoice.DOWNVOTE); + newVote.setVotedBy(user.getId()); + + Review newReview = new Review(); + newReview.setId("newReviewId"); + + when(mongoTemplate.findOne(any(Query.class), eq(Profile.class))).thenReturn(profile); + when(modelMapper.map(requestDto, Vote.class)).thenReturn(newVote); + when(reviewRepository.findById(requestDto.getTypeId())).thenReturn(Optional.of(newReview)); + when(notificationService.createNotification(any(CreateNotificationRequestDto.class))).thenReturn(null); + when(voteRepository.save(any(Vote.class))).thenReturn(newVote); + + Vote result = voteService.addVote(requestDto, user); + + assertNotNull(result); + assertEquals(VoteChoice.DOWNVOTE, result.getChoice()); + assertEquals("newVoteId", result.getId()); + assertEquals(user.getId(), result.getVotedBy()); + } + + @Test + void testAddCommentVoteFirstTime() { + CreateVoteRequestDto requestDto = new CreateVoteRequestDto(); + requestDto.setChoice(VoteChoice.DOWNVOTE.name()); + requestDto.setTypeId("newCommentId"); + requestDto.setVoteType(VoteType.COMMENT.name()); + + User user = new User(); + user.setId("userId"); + user.setUsername("userName"); + + Profile profile = new Profile(); + profile.setUserId("userId"); + profile.setIsVotedYet(false); + + Vote newVote = new Vote(); + newVote.setId("newVoteId"); + newVote.setChoice(VoteChoice.DOWNVOTE); + newVote.setVotedBy(user.getId()); + + Comment newComment = new Comment(); + newComment.setId("newCommentId"); + + when(mongoTemplate.findOne(any(Query.class), eq(Profile.class))).thenReturn(profile); + when(modelMapper.map(requestDto, Vote.class)).thenReturn(newVote); + when(commentRepository.findById(requestDto.getTypeId())).thenReturn(Optional.of(newComment)); + when(notificationService.createNotification(any(CreateNotificationRequestDto.class))).thenReturn(null); + when(voteRepository.save(any(Vote.class))).thenReturn(newVote); + + Vote result = voteService.addVote(requestDto, user); + + assertNotNull(result); + assertEquals(VoteChoice.DOWNVOTE, result.getChoice()); + assertEquals("newVoteId", result.getId()); + assertEquals(user.getId(), result.getVotedBy()); + } + + + @Test + void testAddPostVoteFirstTime() { + CreateVoteRequestDto requestDto = new CreateVoteRequestDto(); + requestDto.setChoice(VoteChoice.DOWNVOTE.name()); + requestDto.setTypeId("newPostId"); + requestDto.setVoteType(VoteType.POST.name()); + + User user = new User(); + user.setId("userId"); + user.setUsername("userName"); + + Profile profile = new Profile(); + profile.setUserId("userId"); + profile.setIsVotedYet(false); + + Vote newVote = new Vote(); + newVote.setId("newVoteId"); + newVote.setChoice(VoteChoice.DOWNVOTE); + newVote.setVotedBy(user.getId()); + + Post newPost = new Post(); + newPost.setId("newPostId"); + + Notification newNotification = new Notification(); + + when(mongoTemplate.findOne(any(Query.class), eq(Profile.class))).thenReturn(profile); + when(modelMapper.map(requestDto, Vote.class)).thenReturn(newVote); + when(postRepository.findById(requestDto.getTypeId())).thenReturn(Optional.of(newPost)); + when(notificationService.createNotification(any(CreateNotificationRequestDto.class))).thenReturn(null); + when(notificationRepository.findByParentAndUser(any(String.class), any(String.class))).thenReturn(Optional.of(newNotification)); + when(voteRepository.save(any(Vote.class))).thenReturn(newVote); + + Vote result = voteService.addVote(requestDto, user); + + assertNotNull(result); + assertEquals(VoteChoice.DOWNVOTE, result.getChoice()); + assertEquals("newVoteId", result.getId()); + assertEquals(user.getId(), result.getVotedBy()); + } + + @Test + void testAddPostVoteFirstTimeWithOverallVote10() { + CreateVoteRequestDto requestDto = new CreateVoteRequestDto(); + requestDto.setChoice(VoteChoice.DOWNVOTE.name()); + requestDto.setTypeId("newPostId"); + requestDto.setVoteType(VoteType.POST.name()); + + User user = new User(); + user.setId("userId"); + user.setUsername("userName"); + + Profile profile = new Profile(); + profile.setUserId("userId"); + profile.setIsVotedYet(false); + + Vote newVote = new Vote(); + newVote.setId("newVoteId"); + newVote.setChoice(VoteChoice.DOWNVOTE); + newVote.setVotedBy(user.getId()); + + Post newPost = new Post(); + newPost.setId("newPostId"); + newPost.setOverallVote(11); // plus 1 since downovote + newPost.setPoster("userId"); + newPost.setTitle("Test Post"); + + Notification newNotification = new Notification(); + + when(userRepository.findById(any(String.class))).thenReturn(Optional.of(user)); + when(mongoTemplate.findOne(any(Query.class), eq(Profile.class))).thenReturn(profile); + when(modelMapper.map(requestDto, Vote.class)).thenReturn(newVote); + when(postRepository.findById(requestDto.getTypeId())).thenReturn(Optional.of(newPost)); + when(notificationService.createNotification(any(CreateNotificationRequestDto.class))).thenReturn(null); + when(notificationRepository.findByParentAndUser(any(String.class), any(String.class))).thenReturn(Optional.of(newNotification)); + when(voteRepository.save(any(Vote.class))).thenReturn(newVote); + + Vote result = voteService.addVote(requestDto, user); + + assertNotNull(result); + assertEquals(VoteChoice.DOWNVOTE, result.getChoice()); + assertEquals("newVoteId", result.getId()); + assertEquals(user.getId(), result.getVotedBy()); + } + + + @Test + void testAddPostVoteFirstTimeWithOverallVote10NoParent() { + CreateVoteRequestDto requestDto = new CreateVoteRequestDto(); + requestDto.setChoice(VoteChoice.DOWNVOTE.name()); + requestDto.setTypeId("newPostId"); + requestDto.setVoteType(VoteType.POST.name()); + + User user = new User(); + user.setId("userId"); + user.setUsername("userName"); + + Profile profile = new Profile(); + profile.setUserId("userId"); + profile.setIsVotedYet(false); + + Vote newVote = new Vote(); + newVote.setId("newVoteId"); + newVote.setChoice(VoteChoice.DOWNVOTE); + newVote.setVotedBy(user.getId()); + + Post newPost = new Post(); + newPost.setId("newPostId"); + newPost.setOverallVote(11); // plus 1 since downovote + newPost.setPoster("userId"); + newPost.setTitle("Test Post"); + + Notification newNotification = new Notification(); + + when(userRepository.findById(any(String.class))).thenReturn(Optional.of(user)); + when(mongoTemplate.findOne(any(Query.class), eq(Profile.class))).thenReturn(profile); + when(modelMapper.map(requestDto, Vote.class)).thenReturn(newVote); + when(postRepository.findById(requestDto.getTypeId())).thenReturn(Optional.of(newPost)); + when(notificationService.createNotification(any(CreateNotificationRequestDto.class))).thenReturn(null); + when(notificationRepository.findByParentAndUser(any(String.class), any(String.class))).thenReturn(Optional.empty()); + when(voteRepository.save(any(Vote.class))).thenReturn(newVote); + + Vote result = voteService.addVote(requestDto, user); + + assertNotNull(result); + assertEquals(VoteChoice.DOWNVOTE, result.getChoice()); + assertEquals("newVoteId", result.getId()); + assertEquals(user.getId(), result.getVotedBy()); + } + + @Test + void testAddVoteResourceNotFound() { + CreateVoteRequestDto requestDto = new CreateVoteRequestDto(); + requestDto.setTypeId("nonExistingId"); + requestDto.setVoteType(VoteType.REVIEW.name()); + + Vote voteToCreate = new Vote(); + + User user = new User(); + user.setId("userId"); + user.setEmail("user@example.com"); + + when(mongoTemplate.findOne(any(Query.class), eq(Profile.class))).thenReturn(null); + when(modelMapper.map(requestDto, Vote.class)).thenReturn(voteToCreate); + + assertThrows(ResourceNotFoundException.class, () -> voteService.addVote(requestDto, user)); + } + + @Test + void testDeleteVoteResourceNotFound() { + String nonExistingVoteId = "nonExistingVoteId"; + when(voteRepository.findById(nonExistingVoteId)).thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, () -> voteService.deleteVote(nonExistingVoteId)); + } + + @Test + void testDeleteVote() { + + Vote vote = new Vote(); + vote.setId("existingVoteId"); + + String existingVoteId = "existingVoteId"; + when(voteRepository.findById(existingVoteId)).thenReturn(Optional.of(vote)); + + Boolean result = voteService.deleteVote(vote.getId()); + + assertNotNull(result); + assertEquals(true,result); + } +} diff --git a/app/backend/src/test/java/com/app/gamereview/services/GameServiceTest.java b/app/backend/src/test/java/com/app/gamereview/services/GameServiceTest.java deleted file mode 100644 index 54f45778..00000000 --- a/app/backend/src/test/java/com/app/gamereview/services/GameServiceTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.app.gamereview.services; - -import com.app.gamereview.dto.response.tag.GetAllTagsOfGameResponseDto; -import com.app.gamereview.exception.ResourceNotFoundException; -import com.app.gamereview.model.Game; -import com.app.gamereview.repository.GameRepository; -import com.app.gamereview.repository.TagRepository; -import com.app.gamereview.service.GameService; -import com.app.gamereview.service.TagService; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.data.mongodb.core.MongoTemplate; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.when; - -class GameServiceTest { - - @Mock - private GameRepository gameRepository; - - @Mock - private TagRepository tagRepository; - - @Mock - private TagService tagService; - - @InjectMocks - private GameService gameService; - - @Mock - private MongoTemplate mongoTemplate; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - - - - // @Test - // void testGetAllGames() { - // // Arrange - // GetGameListRequestDto filter = new GetGameListRequestDto(); - // Game game1 = new Game("Game 1", "Description 1", null, "SystemReq 1"); - // Game game2 = new Game("Game 2", "Description 2", null, "SystemReq 2"); - // List games = List.of(game1, game2); - // when(gameRepository.findAll()).thenReturn(games); - - // // Act - // List response = gameService.getAllGames(filter); - - // // Assert - // assertEquals(2, response.size()); - // assertEquals("Game 1", response.get(0).getGameName()); - // assertEquals("Game 2", response.get(1).getGameName()); - // } -/* - @Test - void testGetGameTags() { - // Arrange - String gameId = "123"; - Game game = new Game("Game 1", "Description 1", null, "SystemReq 1"); - game.setIsDeleted(false); - when(gameRepository.findById(gameId)).thenReturn(Optional.of(game)); - - // Act - GetAllTagsOfGameResponseDto response = gameService.getGameTags(gameId); - - // Assert - assertNotNull(response); - assertEquals(game.getPlayerTypes(), response.getPlayerTypes()); - assertEquals(game.getGenre(), response.getGenre()); - assertEquals(game.getProduction(), response.getProduction()); - assertEquals(game.getDuration(), response.getDuration()); - assertEquals(game.getPlatforms(), response.getPlatforms()); - assertEquals(game.getArtStyles(), response.getArtStyles()); - assertEquals(game.getDeveloper(), response.getDeveloper()); - assertEquals(game.getOtherTags(), response.getOtherTags()); - } - - // @Test - // void testAddGameTag() { - // // Arrange - // String gameId = "123"; - // String tagId = "456"; - // Game game = new Game("Game 1", "Description 1", null, "SystemReq 1"); - // Tag tag = new Tag("Tag 1", com.app.gamereview.enums.TagType.PLAYER_TYPE, "#c06123"); - // game.setIsDeleted(false); - // tag.setIsDeleted(false); - // tag.setId(tagId); - // AddGameTagRequestDto request = new AddGameTagRequestDto(gameId, tagId); - // when(gameRepository.findById(gameId)).thenReturn(Optional.of(game)); - // when(tagRepository.findById(tagId)).thenReturn(Optional.of(tag)); - // CreateTagRequestDto createRequest = new CreateTagRequestDto("Tag 1", com.app.gamereview.enums.TagType.PLAYER_TYPE, "#c06123"); - - // tagService.createTag(createRequest); - // // Act - // AddGameTagResponseDto response = gameService.addGameTag(request); - - // // Assert - // assertNotNull(response); - // assertEquals(gameId, response.getGameId()); - // assertEquals(tag, response.getAddedTag()); - // assertTrue(game.getPlayerTypes().contains(tag)); - // } - - @Test - void testGetGameDetail() { - // Arrange - String gameId = "123"; - Game game = new Game("Game 1", "Description 1", null, "SystemReq 1"); - when(gameRepository.findById(gameId)).thenReturn(Optional.of(game)); - - // Act - var response = gameService.getGameDetail(gameId); - - // Assert - assertNotNull(response); - //assertEquals(game, response.getGame()); - } - - @Test - void testGetGameDetailNotFound() { - // Arrange - String gameId = "123"; - when(gameRepository.findById(gameId)).thenReturn(Optional.empty()); - - // Act & Assert - assertThrows(ResourceNotFoundException.class, () -> gameService.getGameDetail(gameId)); - } - */ - -} diff --git a/app/compose.yml b/app/compose.yml new file mode 100644 index 00000000..d357cf38 --- /dev/null +++ b/app/compose.yml @@ -0,0 +1,24 @@ +version: "3.8" + +services: + frontend: + build: ./frontend + image: lastmusketeer/451_frontend:latest + ports: + - "80:5173" + + backend: + build: ./backend + image: unaldenizzz/bounswe2023group5:latest + ports: + - "3001:8080" + volumes: + - type: bind + source: ./backend/images + target: /app/backend/images + + annotation: + build: ./annotation/annotation + image: nopaew/bounswe2023group5annotation:latest + ports: + - "3002:8080" diff --git a/app/frontend/assets/images/batrider.png b/app/frontend/assets/images/batrider.png new file mode 100644 index 00000000..5ddb296b Binary files /dev/null and b/app/frontend/assets/images/batrider.png differ diff --git a/app/frontend/assets/images/group.png b/app/frontend/assets/images/group.png new file mode 100644 index 00000000..56d321d2 Binary files /dev/null and b/app/frontend/assets/images/group.png differ diff --git a/app/frontend/package.json b/app/frontend/package.json index 57b78d96..198a8753 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -11,12 +11,15 @@ }, "dependencies": { "@ant-design/icons": "^5.2.6", + "@recogito/recogito-js": "^1.8.2", + "@recogito/annotorious": "^2.7.13", "@types/js-cookie": "^3.0.5", "antd": "^5.10.2", "axios": "^1.6.0", "clsx": "^2.0.0", "js-cookie": "^3.0.5", "modern-normalize": "^2.0.0", + "node-vibrant": "3.1.6", "react": "^18.2.0", "react-color": "^2.19.3", "react-datepicker": "^4.21.0", @@ -24,6 +27,7 @@ "react-query": "^3.39.3", "react-router-dom": "^6.17.0", "react-spinners": "^0.13.8", + "react-toastify": "^9.1.3", "sass": "^1.69.4", "tw-to-css": "^0.0.12", "usehooks-ts": "^2.9.1" diff --git a/app/frontend/pnpm-lock.yaml b/app/frontend/pnpm-lock.yaml index 9c76348d..ab7c2271 100644 --- a/app/frontend/pnpm-lock.yaml +++ b/app/frontend/pnpm-lock.yaml @@ -8,6 +8,12 @@ dependencies: '@ant-design/icons': specifier: ^5.2.6 version: 5.2.6(react-dom@18.2.0)(react@18.2.0) + '@recogito/annotorious': + specifier: ^2.7.13 + version: 2.7.13(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) + '@recogito/recogito-js': + specifier: ^1.8.2 + version: 1.8.4(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) '@types/js-cookie': specifier: ^3.0.5 version: 3.0.5 @@ -26,6 +32,9 @@ dependencies: modern-normalize: specifier: ^2.0.0 version: 2.0.0 + node-vibrant: + specifier: 3.1.6 + version: 3.1.6 react: specifier: ^18.2.0 version: 18.2.0 @@ -47,6 +56,9 @@ dependencies: react-spinners: specifier: ^0.13.8 version: 0.13.8(react-dom@18.2.0)(react@18.2.0) + react-toastify: + specifier: ^9.1.3 + version: 9.1.3(react-dom@18.2.0)(react@18.2.0) sass: specifier: ^1.69.4 version: 1.69.4 @@ -177,7 +189,6 @@ packages: dependencies: '@babel/highlight': 7.22.20 chalk: 2.4.2 - dev: true /@babel/compat-data@7.23.2: resolution: {integrity: sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==} @@ -253,7 +264,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.23.0 - dev: true /@babel/helper-module-transforms@7.23.0(@babel/core@7.23.2): resolution: {integrity: sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==} @@ -291,12 +301,10 @@ packages: /@babel/helper-string-parser@7.22.5: resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-option@7.22.15: resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} @@ -321,7 +329,6 @@ packages: '@babel/helper-validator-identifier': 7.22.20 chalk: 2.4.2 js-tokens: 4.0.0 - dev: true /@babel/parser@7.23.0: resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} @@ -391,21 +398,109 @@ packages: '@babel/helper-string-parser': 7.22.5 '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - dev: true /@ctrl/tinycolor@3.6.1: resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} engines: {node: '>=10'} dev: false + /@emotion/babel-plugin@11.11.0: + resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} + dependencies: + '@babel/helper-module-imports': 7.22.15 + '@babel/runtime': 7.23.2 + '@emotion/hash': 0.9.1 + '@emotion/memoize': 0.8.1 + '@emotion/serialize': 1.1.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + dev: false + + /@emotion/cache@11.11.0: + resolution: {integrity: sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==} + dependencies: + '@emotion/memoize': 0.8.1 + '@emotion/sheet': 1.2.2 + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + stylis: 4.2.0 + dev: false + /@emotion/hash@0.8.0: resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} dev: false + /@emotion/hash@0.9.1: + resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} + dev: false + + /@emotion/memoize@0.8.1: + resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} + dev: false + + /@emotion/react@11.11.3(@types/react@18.2.28)(react@18.2.0): + resolution: {integrity: sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@emotion/babel-plugin': 11.11.0 + '@emotion/cache': 11.11.0 + '@emotion/serialize': 1.1.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + '@types/react': 18.2.28 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + dev: false + + /@emotion/serialize@1.1.3: + resolution: {integrity: sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==} + dependencies: + '@emotion/hash': 0.9.1 + '@emotion/memoize': 0.8.1 + '@emotion/unitless': 0.8.1 + '@emotion/utils': 1.2.1 + csstype: 3.1.2 + dev: false + + /@emotion/sheet@1.2.2: + resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} + dev: false + /@emotion/unitless@0.7.5: resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==} dev: false + /@emotion/unitless@0.8.1: + resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} + dev: false + + /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0): + resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + dev: false + + /@emotion/utils@1.2.1: + resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==} + dev: false + + /@emotion/weak-memoize@0.3.1: + resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} + dev: false + /@esbuild/android-arm64@0.18.20: resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -669,6 +764,116 @@ packages: react: 18.2.0 dev: false + /@jimp/bmp@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-9edAxu7N2FX7vzkdl5Jo1BbACfycUtBQX+XBMcHA2bk62P8R0otgkHg798frgAk/WxQIzwxqOH6wMiCwrlAzdQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.2 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + bmp-js: 0.1.0 + dev: false + + /@jimp/core@0.16.13: + resolution: {integrity: sha512-qXpA1tzTnlkTku9yqtuRtS/wVntvE6f3m3GNxdTdtmc+O+Wcg9Xo2ABPMh7Nc0AHbMKzwvwgB2JnjZmlmJEObg==} + dependencies: + '@babel/runtime': 7.23.2 + '@jimp/utils': 0.16.13 + any-base: 1.1.0 + buffer: 5.7.1 + exif-parser: 0.1.12 + file-type: 16.5.4 + load-bmfont: 1.4.1 + mkdirp: 0.5.6 + phin: 2.9.3 + pixelmatch: 4.0.2 + tinycolor2: 1.6.0 + dev: false + + /@jimp/custom@0.16.13: + resolution: {integrity: sha512-LTATglVUPGkPf15zX1wTMlZ0+AU7cGEGF6ekVF1crA8eHUWsGjrYTB+Ht4E3HTrCok8weQG+K01rJndCp/l4XA==} + dependencies: + '@babel/runtime': 7.23.2 + '@jimp/core': 0.16.13 + dev: false + + /@jimp/gif@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-yFAMZGv3o+YcjXilMWWwS/bv1iSqykFahFMSO169uVMtfQVfa90kt4/kDwrXNR6Q9i6VHpFiGZMlF2UnHClBvg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.2 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + gifwrap: 0.9.4 + omggif: 1.0.10 + dev: false + + /@jimp/jpeg@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-BJHlDxzTlCqP2ThqP8J0eDrbBfod7npWCbJAcfkKqdQuFk0zBPaZ6KKaQKyKxmWJ87Z6ohANZoMKEbtvrwz1AA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.2 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + jpeg-js: 0.4.4 + dev: false + + /@jimp/plugin-resize@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-qoqtN8LDknm3fJm9nuPygJv30O3vGhSBD2TxrsCnhtOsxKAqVPJtFVdGd/qVuZ8nqQANQmTlfqTiK9mVWQ7MiQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.2 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + dev: false + + /@jimp/png@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-8cGqINvbWJf1G0Her9zbq9I80roEX0A+U45xFby3tDWfzn+Zz8XKDF1Nv9VUwVx0N3zpcG1RPs9hfheG4Cq2kg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.2 + '@jimp/custom': 0.16.13 + '@jimp/utils': 0.16.13 + pngjs: 3.4.0 + dev: false + + /@jimp/tiff@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-oJY8d9u95SwW00VPHuCNxPap6Q1+E/xM5QThb9Hu+P6EGuu6lIeLaNBMmFZyblwFbwrH+WBOZlvIzDhi4Dm/6Q==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.2 + '@jimp/custom': 0.16.13 + utif: 2.0.1 + dev: false + + /@jimp/types@0.16.13(@jimp/custom@0.16.13): + resolution: {integrity: sha512-mC0yVNUobFDjoYLg4hoUwzMKgNlxynzwt3cDXzumGvRJ7Kb8qQGOWJQjQFo5OxmGExqzPphkirdbBF88RVLBCg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + dependencies: + '@babel/runtime': 7.23.2 + '@jimp/bmp': 0.16.13(@jimp/custom@0.16.13) + '@jimp/custom': 0.16.13 + '@jimp/gif': 0.16.13(@jimp/custom@0.16.13) + '@jimp/jpeg': 0.16.13(@jimp/custom@0.16.13) + '@jimp/png': 0.16.13(@jimp/custom@0.16.13) + '@jimp/tiff': 0.16.13(@jimp/custom@0.16.13) + timm: 1.7.1 + dev: false + + /@jimp/utils@0.16.13: + resolution: {integrity: sha512-VyCpkZzFTHXtKgVO35iKN0sYR10psGpV6SkcSeV4oF7eSYlR8Bl6aQLCzVeFjvESF7mxTmIiI3/XrMobVrtxDA==} + dependencies: + '@babel/runtime': 7.23.2 + regenerator-runtime: 0.13.11 + dev: false + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -809,11 +1014,58 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@recogito/annotorious@2.7.13(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-dcNK+bCv7Mcq/KExCVDm3mfTYYGVsll5VbOg15m0IQszjq7Ra4bhponUGDrFpqingeiaZ56cQSRqygd/7RNKMQ==} + dependencies: + '@recogito/recogito-client-core': 1.7.9(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) + preact: 10.19.3 + tiny-emitter: 2.1.0 + transitivePeerDependencies: + - '@types/react' + - react + - react-dom + dev: false + + /@recogito/recogito-client-core@1.7.9(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-fPkBUlNP5WDwlqPKUDLB0O6DnZQG2mtOBiS78wLJjbbyU8b8yYt8RN9VVui1gji3diCPz+oWpH22eWZTQlADlQ==} + dependencies: + core-js: 3.34.0 + fast-deep-equal: 3.1.3 + node-polyglot: 2.5.0 + react-autosize-textarea: 7.1.0(react-dom@18.2.0)(react@18.2.0) + react-draggable: 4.4.6(react-dom@18.2.0)(react@18.2.0) + react-select: 4.3.1(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + regenerator-runtime: 0.13.11 + timeago-react: 3.0.6(react@18.2.0) + uuid: 8.3.2 + transitivePeerDependencies: + - '@types/react' + - react + - react-dom + dev: false + + /@recogito/recogito-js@1.8.4(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Pnp3YURYx8MZSEBMXrMUnbuNu6+P2PzSeHFWCzFbGMCMW+aiBAvODP9fNf+s9hz/tlo9MpReAZg6aJbVU7Qbig==} + dependencies: + '@recogito/recogito-client-core': 1.7.9(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0) + preact: 10.19.3 + tiny-emitter: 2.1.0 + transitivePeerDependencies: + - '@types/react' + - react + - react-dom + dev: false + /@remix-run/router@1.10.0: resolution: {integrity: sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw==} engines: {node: '>=14.0.0'} dev: false + /@tokenizer/token@0.3.0: + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + dev: false + /@types/babel__core@7.20.2: resolution: {integrity: sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==} dependencies: @@ -851,9 +1103,24 @@ packages: resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==} dev: true + /@types/lodash@4.14.202: + resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} + dev: false + + /@types/node@10.17.60: + resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} + dev: false + + /@types/node@16.9.1: + resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} + dev: false + + /@types/parse-json@4.0.2: + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + dev: false + /@types/prop-types@15.7.8: resolution: {integrity: sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==} - dev: true /@types/react-color@3.0.10: resolution: {integrity: sha512-6K5BAn3zyd8lW8UbckIAVeXGxR82Za9jyGD2DBEynsa7fKaguLDVtjfypzs7fgEV7bULgs7uhds8A8v1wABTvQ==} @@ -886,7 +1153,6 @@ packages: '@types/prop-types': 15.7.8 '@types/scheduler': 0.16.4 csstype: 3.1.2 - dev: true /@types/reactcss@1.2.9: resolution: {integrity: sha512-dN2TtynLIZaZZ4gQNK6WM0Nff8GWYCXKl1Kvsp59WgROtx03ixCwuC1UWdesgt2O1P5Qk+0+SIfsy3eiwblMEA==} @@ -896,7 +1162,6 @@ packages: /@types/scheduler@0.16.4: resolution: {integrity: sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==} - dev: true /@types/semver@7.5.3: resolution: {integrity: sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==} @@ -1082,7 +1347,6 @@ packages: engines: {node: '>=4'} dependencies: color-convert: 1.9.3 - dev: true /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} @@ -1153,6 +1417,10 @@ packages: - moment dev: false + /any-base@1.1.0: + resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} + dev: false + /any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} dev: false @@ -1172,6 +1440,13 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.5 + is-array-buffer: 3.0.2 + dev: false + /array-tree-filter@2.1.0: resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==} dev: false @@ -1181,6 +1456,31 @@ packages: engines: {node: '>=8'} dev: true + /array.prototype.foreach@1.0.5: + resolution: {integrity: sha512-FSk2BdZDQVdxGeh63usPldJo5xtkdBp3iYBqEGlGnId5TV0xtrKOnz9kXzfFL5L/81EIuVkxtiYtJSE2IjKoPA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-array-method-boxes-properly: 1.0.0 + get-intrinsic: 1.2.2 + is-string: 1.0.7 + dev: false + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: false + /async-validator@4.2.5: resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} dev: false @@ -1189,6 +1489,15 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false + /autosize@4.0.4: + resolution: {integrity: sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ==} + dev: false + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: false + /axios@1.6.0: resolution: {integrity: sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==} dependencies: @@ -1199,9 +1508,22 @@ packages: - debug dev: false + /babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + dependencies: + '@babel/runtime': 7.23.2 + cosmiconfig: 7.1.0 + resolve: 1.22.8 + dev: false + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + /big-integer@1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} engines: {node: '>=0.6'} @@ -1211,6 +1533,10 @@ packages: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} + /bmp-js@0.1.0: + resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==} + dev: false + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -1247,10 +1573,29 @@ packages: update-browserslist-db: 1.0.13(browserslist@4.22.1) dev: true + /buffer-equal@0.0.1: + resolution: {integrity: sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==} + engines: {node: '>=0.4.0'} + dev: false + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + dev: false + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - dev: true /camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} @@ -1268,7 +1613,6 @@ packages: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - dev: true /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -1296,6 +1640,11 @@ packages: resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} dev: false + /clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + dev: false + /clsx@2.0.0: resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} engines: {node: '>=6'} @@ -1305,7 +1654,6 @@ packages: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: color-name: 1.1.3 - dev: true /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} @@ -1316,7 +1664,6 @@ packages: /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -1338,9 +1685,17 @@ packages: resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} dev: false + /computed-style@0.1.4: + resolution: {integrity: sha512-WpAmaKbMNmS3OProfHIdJiNleNJdgUrJfbKArXua28QF7+0CoZjlLn0lp6vlc+dl5r2/X9GQiQRQQU4BzSa69w==} + dev: false + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + /convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + dev: false + /convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true @@ -1351,6 +1706,22 @@ packages: toggle-selection: 1.0.6 dev: false + /core-js@3.34.0: + resolution: {integrity: sha512-aDdvlDder8QmY91H88GzNi9EtQi2TjvQhpCX6B1v/dAZHU1AuLgHvRh54RiOerpEhEW46Tkf+vgAViB/CWC0ag==} + requiresBuild: true + dev: false + + /cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1395,6 +1766,24 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + has-property-descriptors: 1.0.1 + object-keys: 1.1.1 + dev: false + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1426,10 +1815,94 @@ packages: esutils: 2.0.3 dev: true + /dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dependencies: + '@babel/runtime': 7.23.2 + csstype: 3.1.2 + dev: false + + /dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + dev: false + /electron-to-chromium@1.4.554: resolution: {integrity: sha512-Q0umzPJjfBrrj8unkONTgbKQXzXRrH7sVV7D9ea2yBV3Oaogz991yhbpfvo2LMNkJItmruXTEzVpP9cp7vaIiQ==} dev: true + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: false + + /es-abstract@1.22.3: + resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + es-set-tostringtag: 2.0.2 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.2 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + internal-slot: 1.0.6 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.13 + dev: false + + /es-array-method-boxes-properly@1.0.0: + resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} + dev: false + + /es-set-tostringtag@2.0.2: + resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + has-tostringtag: 1.0.0 + hasown: 2.0.0 + dev: false + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: false + /esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} @@ -1472,7 +1945,6 @@ packages: /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - dev: true /eslint-plugin-react-hooks@4.6.0(eslint@8.51.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} @@ -1583,13 +2055,16 @@ packages: engines: {node: '>=0.10.0'} dev: true + /exif-parser@0.1.12: + resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} + dev: false + /extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} dev: false /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} @@ -1621,12 +2096,25 @@ packages: flat-cache: 3.1.1 dev: true + /file-type@16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + dependencies: + readable-web-to-node-stream: 3.0.2 + strtok3: 6.3.0 + token-types: 4.2.1 + dev: false + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 + /find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + dev: false + /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1658,6 +2146,12 @@ packages: optional: true dev: false + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -1681,11 +2175,49 @@ packages: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} dev: false + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + functions-have-names: 1.2.3 + dev: false + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: false + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} dev: true + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: false + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + dev: false + + /gifwrap@0.9.4: + resolution: {integrity: sha512-MDMwbhASQuVeD4JKd1fKgNgCRL3fGqMM4WaqpNhWO0JiMOAjbQdumbs4BbBZEy9/M00EHEjKN3HieVhCUlwjeQ==} + dependencies: + image-q: 4.0.0 + omggif: 1.0.10 + dev: false + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1719,6 +2251,13 @@ packages: once: 1.4.0 path-is-absolute: 1.0.1 + /global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + dependencies: + min-document: 2.19.0 + process: 0.11.10 + dev: false + /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -1731,6 +2270,13 @@ packages: type-fest: 0.20.2 dev: true + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: false + /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -1743,20 +2289,57 @@ packages: slash: 3.0.0 dev: true + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: false + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} dev: true + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /has@1.0.4: + resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} + engines: {node: '>= 0.4.0'} + dev: false + /hasown@2.0.0: resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} engines: {node: '>= 0.4'} @@ -1764,11 +2347,27 @@ packages: function-bind: 1.1.2 dev: false + /hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + dependencies: + react-is: 16.13.1 + dev: false + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} dev: true + /image-q@4.0.0: + resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} + dependencies: + '@types/node': 16.9.1 + dev: false + /immutable@4.3.4: resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} @@ -1778,7 +2377,6 @@ packages: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - dev: true /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} @@ -1794,28 +2392,91 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /internal-slot@1.0.6: + resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + hasown: 2.0.0 + side-channel: 1.0.4 + dev: false + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 + dev: false + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: false + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: false + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} dependencies: binary-extensions: 2.2.0 + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: false + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: false + /is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: hasown: 2.0.0 dev: false + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + /is-function@1.0.2: + resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==} + dev: false + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: false + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1825,6 +2486,51 @@ packages: engines: {node: '>=8'} dev: true + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: false + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.5 + dev: false + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.13 + dev: false + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.5 + dev: false + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: false + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true @@ -1834,6 +2540,10 @@ packages: hasBin: true dev: false + /jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + dev: false + /js-cookie@3.0.5: resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} engines: {node: '>=14'} @@ -1863,6 +2573,10 @@ packages: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: true + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: false + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true @@ -1902,10 +2616,30 @@ packages: engines: {node: '>=10'} dev: false + /line-height@0.3.1: + resolution: {integrity: sha512-YExecgqPwnp5gplD2+Y8e8A5+jKpr25+DzMbFdI1/1UAr0FJrTFv4VkHLf8/6B590i1wUPJWMKKldkd/bdQ//w==} + engines: {node: '>= 4.0.0'} + dependencies: + computed-style: 0.1.4 + dev: false + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: false + /load-bmfont@1.4.1: + resolution: {integrity: sha512-8UyQoYmdRDy81Brz6aLAUhfZLwr5zV0L3taTQ4hju7m6biuwiWiJXjPhBJxbUQJA8PrkvJ/7Enqmwk2sM14soA==} + dependencies: + buffer-equal: 0.0.1 + mime: 1.6.0 + parse-bmfont-ascii: 1.0.6 + parse-bmfont-binary: 1.0.6 + parse-bmfont-xml: 1.1.4 + phin: 2.9.3 + xhr: 2.6.0 + xtend: 4.0.2 + dev: false + /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1955,6 +2689,10 @@ packages: resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} dev: false + /memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + dev: false + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1982,11 +2720,34 @@ packages: mime-db: 1.52.0 dev: false + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /min-document@2.19.0: + resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} + dependencies: + dom-walk: 0.1.2 + dev: false + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: false + + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: false + /modern-normalize@2.0.0: resolution: {integrity: sha512-CxBoEVKh5U4DH3XuNbc5ONLF6dQBc8dSc7pdZ1957FGbIO5JBqGqqchhET9dTexri8/pk9xBL6+5ceOtCIp1QA==} engines: {node: '>=6'} @@ -2019,10 +2780,32 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /node-polyglot@2.5.0: + resolution: {integrity: sha512-zXVwHNhFsG3mls+LKHxoHF70GQOL3FTDT3jH7ldkb95kG76RdU7F/NbvxV7D2hNIL9VpWXW6y78Fz+3KZkatRg==} + dependencies: + array.prototype.foreach: 1.0.5 + has: 1.0.4 + object.entries: 1.1.7 + string.prototype.trim: 1.2.8 + warning: 4.0.3 + dev: false + /node-releases@2.0.13: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} dev: true + /node-vibrant@3.1.6: + resolution: {integrity: sha512-Wlc/hQmBMOu6xon12ZJHS2N3M+I6J8DhrD3Yo6m5175v8sFkVIN+UjhKVRcO+fqvre89ASTpmiFEP3nPO13SwA==} + dependencies: + '@jimp/custom': 0.16.13 + '@jimp/plugin-resize': 0.16.13(@jimp/custom@0.16.13) + '@jimp/types': 0.16.13(@jimp/custom@0.16.13) + '@types/lodash': 4.14.202 + '@types/node': 10.17.60 + lodash: 4.17.21 + url: 0.11.3 + dev: false + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -2037,10 +2820,42 @@ packages: engines: {node: '>= 6'} dev: false + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: false + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: false + + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: false + + /object.entries@1.1.7: + resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: false + /oblivious-set@1.0.0: resolution: {integrity: sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==} dev: false + /omggif@1.0.10: + resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -2072,12 +2887,44 @@ packages: p-limit: 3.1.0 dev: true + /pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + dev: false + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} dependencies: callsites: 3.1.0 - dev: true + + /parse-bmfont-ascii@1.0.6: + resolution: {integrity: sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==} + dev: false + + /parse-bmfont-binary@1.0.6: + resolution: {integrity: sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==} + dev: false + + /parse-bmfont-xml@1.1.4: + resolution: {integrity: sha512-bjnliEOmGv3y1aMEfREMBJ9tfL3WR0i0CKPj61DnSLaoxWR3nLrsQrEbCId/8rF4NyRF0cCqisSVXyQYWM+mCQ==} + dependencies: + xml-parse-from-string: 1.0.1 + xml2js: 0.4.23 + dev: false + + /parse-headers@2.0.5: + resolution: {integrity: sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==} + dev: false + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.22.13 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: false /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} @@ -2100,7 +2947,15 @@ packages: /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - dev: true + + /peek-readable@4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + dev: false + + /phin@2.9.3: + resolution: {integrity: sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==} + dev: false /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -2119,6 +2974,18 @@ packages: engines: {node: '>= 6'} dev: false + /pixelmatch@4.0.2: + resolution: {integrity: sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==} + hasBin: true + dependencies: + pngjs: 3.4.0 + dev: false + + /pngjs@3.4.0: + resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} + engines: {node: '>=4.0.0'} + dev: false + /postcss-css-variables@0.18.0(postcss@8.4.31): resolution: {integrity: sha512-lYS802gHbzn1GI+lXvy9MYIYDuGnl1WB4FTKoqMQqJ3Mab09A7a/1wZvGTkCEZJTM8mSbIyb1mJYn8f0aPye0Q==} peerDependencies: @@ -2199,11 +3066,20 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /preact@10.19.3: + resolution: {integrity: sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==} + dev: false + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} dev: true + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: false + /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} dependencies: @@ -2216,6 +3092,10 @@ packages: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false + /punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + dev: false + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -2229,6 +3109,13 @@ packages: react: 18.2.0 dev: false + /qs@6.11.2: + resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2742,6 +3629,19 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-autosize-textarea@7.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BHpjCDkuOlllZn3nLazY2F8oYO1tS2jHnWhcjTWQdcKiiMU6gHLNt/fzmqMSyerR0eTdKtfSIqtSeTtghNwS+g==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 + react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0 + dependencies: + autosize: 4.0.4 + line-height: 0.3.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-color@2.19.3(react@18.2.0): resolution: {integrity: sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==} peerDependencies: @@ -2782,9 +3682,30 @@ packages: react: 18.2.0 scheduler: 0.23.0 + /react-draggable@4.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==} + peerDependencies: + react: '>= 16.3.0' + react-dom: '>= 16.3.0' + dependencies: + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + /react-input-autosize@3.0.0(react@18.2.0): + resolution: {integrity: sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg==} + peerDependencies: + react: ^16.3.0 || ^17.0.0 + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: false @@ -2863,6 +3784,25 @@ packages: react: 18.2.0 dev: false + /react-select@4.3.1(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HBBd0dYwkF5aZk1zP81Wx5UsLIIT2lSvAY2JiJo199LjoLHoivjn9//KsmvQMEFGNhe58xyuOITjfxKCcGc62Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + dependencies: + '@babel/runtime': 7.23.2 + '@emotion/cache': 11.11.0 + '@emotion/react': 11.11.3(@types/react@18.2.28)(react@18.2.0) + memoize-one: 5.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-input-autosize: 3.0.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /react-spinners@0.13.8(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==} peerDependencies: @@ -2873,6 +3813,31 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-toastify@9.1.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==} + peerDependencies: + react: '>=16' + react-dom: '>=16' + dependencies: + clsx: 1.2.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + dependencies: + '@babel/runtime': 7.23.2 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -2894,15 +3859,44 @@ packages: pify: 2.3.0 dev: false + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /readable-web-to-node-stream@3.0.2: + resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==} + engines: {node: '>=8'} + dependencies: + readable-stream: 3.6.2 + dev: false + /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} dependencies: picomatch: 2.3.1 + /regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + dev: false + /regenerator-runtime@0.14.0: resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + set-function-name: 2.0.1 + dev: false + /remove-accents@0.4.2: resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} dev: false @@ -2914,7 +3908,6 @@ packages: /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - dev: true /resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} @@ -2948,6 +3941,28 @@ packages: dependencies: queue-microtask: 1.2.3 + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-regex: 1.1.4 + dev: false + /sass@1.69.4: resolution: {integrity: sha512-+qEreVhqAy8o++aQfCJwp0sklr2xyEzkm9Pp/Igu9wNPoe7EZEQ8X/MBvvXggI2ql607cxKg/RKOwDj6pp2XDA==} engines: {node: '>=14.0.0'} @@ -2957,6 +3972,10 @@ packages: immutable: 4.3.4 source-map-js: 1.0.2 + /sax@1.3.0: + resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} + dev: false + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: @@ -2981,6 +4000,25 @@ packages: lru-cache: 6.0.0 dev: true + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.1 + dev: false + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2993,6 +4031,14 @@ packages: engines: {node: '>=8'} dev: true + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + object-inspect: 1.13.1 + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -3002,10 +4048,46 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + /source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + dev: false + /string-convert@0.2.1: resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} dev: false + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: false + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: false + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: false + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3018,6 +4100,18 @@ packages: engines: {node: '>=8'} dev: true + /strtok3@6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 4.1.0 + dev: false + + /stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + dev: false + /stylis@4.3.0: resolution: {integrity: sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==} dev: false @@ -3041,7 +4135,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -3109,6 +4202,27 @@ packages: engines: {node: '>=12.22'} dev: false + /timeago-react@3.0.6(react@18.2.0): + resolution: {integrity: sha512-4ywnCX3iFjdp84WPK7gt8s4n0FxXbYM+xv8hYL73p83dpcMxzmO+0W4xJuxflnkWNvum5aEaqTe6LZ3lUIudjQ==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + timeago.js: 4.0.2 + dev: false + + /timeago.js@4.0.2: + resolution: {integrity: sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==} + dev: false + + /timm@1.7.1: + resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==} + dev: false + + /tiny-emitter@2.1.0: + resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==} + dev: false + /tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} dev: false @@ -3116,7 +4230,6 @@ packages: /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} - dev: true /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -3128,6 +4241,14 @@ packages: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} dev: false + /token-types@4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + dev: false + /ts-api-utils@1.0.3(typescript@5.2.2): resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} engines: {node: '>=16.13.0'} @@ -3164,12 +4285,59 @@ packages: engines: {node: '>=10'} dev: true + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: false + /typescript@5.2.2: resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} engines: {node: '>=14.17'} hasBin: true dev: true + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.5 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: false + /unload@2.2.0: resolution: {integrity: sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==} dependencies: @@ -3194,6 +4362,13 @@ packages: punycode: 2.3.0 dev: true + /url@0.11.3: + resolution: {integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==} + dependencies: + punycode: 1.4.1 + qs: 6.11.2 + dev: false + /usehooks-ts@2.9.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA==} engines: {node: '>=16.15.0', npm: '>=8'} @@ -3205,10 +4380,21 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /utif@2.0.1: + resolution: {integrity: sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==} + dependencies: + pako: 1.0.11 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + /vite@4.4.11(sass@1.69.4): resolution: {integrity: sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==} engines: {node: ^14.18.0 || >=16.0.0} @@ -3250,6 +4436,27 @@ packages: dependencies: loose-envify: 1.4.0 + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: false + + /which-typed-array@1.1.13: + resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: false + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3261,6 +4468,37 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /xhr@2.6.0: + resolution: {integrity: sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==} + dependencies: + global: 4.4.0 + is-function: 1.0.2 + parse-headers: 2.0.5 + xtend: 4.0.2 + dev: false + + /xml-parse-from-string@1.0.1: + resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} + dev: false + + /xml2js@0.4.23: + resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} + engines: {node: '>=4.0.0'} + dependencies: + sax: 1.3.0 + xmlbuilder: 11.0.1 + dev: false + + /xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + dev: false + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: false + /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} dev: true @@ -3269,6 +4507,11 @@ packages: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: false + /yaml@2.3.4: resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} engines: {node: '>= 14'} diff --git a/app/frontend/src/App.tsx b/app/frontend/src/App.tsx index 74e1cbae..00713a5a 100644 --- a/app/frontend/src/App.tsx +++ b/app/frontend/src/App.tsx @@ -2,11 +2,23 @@ import { RouterProvider } from "react-router-dom"; import { router } from "./router"; import "./globals.scss"; import AntdConfigProvider from "./Components/Providers/AntdConfigProvider"; +import { ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; + function App() { return ( + ); } diff --git a/app/frontend/src/Components/Achievement/Achievement/Achievement.module.scss b/app/frontend/src/Components/Achievement/Achievement/Achievement.module.scss index 57099ae6..7442959c 100644 --- a/app/frontend/src/Components/Achievement/Achievement/Achievement.module.scss +++ b/app/frontend/src/Components/Achievement/Achievement/Achievement.module.scss @@ -1,77 +1,69 @@ @import "../../../colors"; .achievement { - display: flex; - align-items: center; - margin: 10px; - padding: 10px; - border: 1px solid #ccc; - border-radius: 8px; - transition: border-color 0.3s; - width: 20em; - height: 6em; - background-color: $violet-light-30; - } + display: flex; + align-items: center; + padding: 10px; + border: 1px solid #ccc; + border-radius: 8px; + transition: border-color 0.3s; + width: 20em; + height: 6em; + background-color: $violet-light-30; +} - .achievement:hover { - border-color: #007bff; /* Highlight border on hover */ - width: 20em; - height: 6em; - } +.achievement:hover { + border-color: #007bff; /* Highlight border on hover */ + width: 20em; + height: 6em; + opacity: 0.7; +} - .icon { - margin-right: 10px; - border-radius: 10%; - width: 25%; - height: 100%; - } +.icon { + margin-right: 10px; + border-radius: 10%; + width: 25%; + height: 100%; +} - .achievement_content { - flex: 1; - } - - .hovered .achievementTitle { - visibility: hidden; - } - .achievementTitle { - visibility: visible; - font-size: 18px; - } +.hovered .achievementTitle { + visibility: hidden; +} +.achievementTitle { + visibility: visible; + width: 10em; + font-size: 18px; +} - .hovered .achievementGame { - visibility: hidden; - } - .achievementGame { - visibility: visible; - font-size: 14px; - } - - - .achievement_description { - font-size: 14px; - color: #555; - } - .hovered .floatingDescription { - visibility: visible; - color: #fff; - text-align: center; - align-items: center; - border-radius: 6px; - padding: 5px 0; - z-index: 1; - } - .floatingDescription { - visibility: hidden; - color: #fff; - text-align: center ; - border-radius: 6px; - padding: 5px 0; - position: absolute; - z-index: 1; - } - .row { - display: flex; - flex-direction: row; - align-items: center; - position: relative; - margin-left: 10px; - } \ No newline at end of file +.hovered .achievementGame { + visibility: hidden; +} +.achievementGame { + visibility: visible; + font-size: 14px; +} + +.hovered .floatingDescription { + visibility: visible; + color: #fff; + text-align: center; + align-items: center; + border-radius: 6px; + padding: 5px 0; + z-index: 1; +} +.floatingDescription { + visibility: hidden; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px 0; + position: absolute; + z-index: 1; +} +.row { + display: flex; + flex-direction: row; + align-items: center; + position: relative; + margin-left: 10px; +} diff --git a/app/frontend/src/Components/Achievement/Achievement/Achievement.tsx b/app/frontend/src/Components/Achievement/Achievement/Achievement.tsx index 1582d7da..4e9bef59 100644 --- a/app/frontend/src/Components/Achievement/Achievement/Achievement.tsx +++ b/app/frontend/src/Components/Achievement/Achievement/Achievement.tsx @@ -1,42 +1,53 @@ -import React, { useState } from 'react'; -import styles from './Achievement.module.scss'; +import React, { useState } from "react"; +import styles from "./Achievement.module.scss"; +import { useNavigate } from "react-router-dom"; - -function Achievement ({ props }:{ props: any }) { +function Achievement({ props }: { props: any }) { + const navigate = useNavigate(); const [isHovered, setIsHovered] = useState(false); const handleMouseOver = () => { setIsHovered(true); }; - const handleMouseLeave = () => { setIsHovered(false); }; + function handleClick() { + if (props.onClick) { + props.onClick(); + } else { + if (!props.game) { + return; + } + const gameId = props.game; + navigate(`/game/detail/${gameId}`); + } + } + return (
-
+
achievement icon - -
-
-

{props.title}

-

{props.game.gameName}

-

{props.description}

-
-
- + src={`${import.meta.env.VITE_APP_IMG_URL}${props?.icon}`} + alt="achievement icon" + width={120} + height={100} + className={styles.icon} + > + +
+

{props.title}

+

{props.description}

- +
); -}; - +} -export default Achievement; \ No newline at end of file +export default Achievement; diff --git a/app/frontend/src/Components/Achievement/SquareAchievement/SquareAchievement.module.scss b/app/frontend/src/Components/Achievement/SquareAchievement/SquareAchievement.module.scss index 7affc4ca..ef0db6a0 100644 --- a/app/frontend/src/Components/Achievement/SquareAchievement/SquareAchievement.module.scss +++ b/app/frontend/src/Components/Achievement/SquareAchievement/SquareAchievement.module.scss @@ -1,58 +1,57 @@ @import "../../../colors"; .achievement { - display: flex; - align-items: center; - margin: 10px; - padding: 8px; - border: 1px solid #ccc; - border-radius: 8px; - transition: border-color 0.3s; - width: 10%; - height: 10%; - background-color: $violet-light-30; - } - - - - .icon { - border-radius: 10%; - width: 100%; - height: 100%; - } - - .achievement_content { - flex: 1; - } - - - .achievementTitle { - visibility: visible; - font-size: 18px; - } - - .achievementGame { - visibility: visible; - font-size: 14px; - } - - - .achievement_description { - font-size: 14px; - color: #555; - } - - .floatingDescription { - visibility: hidden; - color: #fff; - text-align: center ; - border-radius: 6px; - padding: 5px 0; - position: absolute; - z-index: 1; - } - - .tooltip{ - position: relative; - display: inline-block; - border-bottom: 1px dotted black; - } + display: flex; + align-items: center; + padding: 8px; + border: 1px solid #ccc; + border-radius: 8px; + transition: border-color 0.3s; + width: 100px; + height: 100px; + background-color: $violet-light-30; +} + +.icon:hover{ + opacity: 0.7; +} +.icon { + border-radius: 10%; + width: 100%; + height: 100%; +} + +.achievement_content { + flex: 1; +} + +.achievementTitle { + visibility: visible; + font-size: 18px; +} + +.achievementGame { + visibility: visible; + font-size: 14px; +} + +.achievement_description { + font-size: 14px; + color: #555; +} + +.floatingDescription { + visibility: hidden; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px 0; + position: absolute; + z-index: 1; +} + +.tooltip { + position: relative; + display: inline-block; + border-bottom: 1px dotted black; + padding: 0; +} diff --git a/app/frontend/src/Components/Achievement/SquareAchievement/SquareAchievement.tsx b/app/frontend/src/Components/Achievement/SquareAchievement/SquareAchievement.tsx index 74fe5ffb..a07e7fc2 100644 --- a/app/frontend/src/Components/Achievement/SquareAchievement/SquareAchievement.tsx +++ b/app/frontend/src/Components/Achievement/SquareAchievement/SquareAchievement.tsx @@ -1,31 +1,43 @@ -import React, { useState } from 'react'; -import styles from './SquareAchievement.module.scss'; -import { Tooltip } from 'antd'; +import React, { useState } from "react"; +import styles from "./SquareAchievement.module.scss"; +import { Tooltip } from "antd"; +import { useNavigate } from "react-router-dom"; +function SquareAchievement({ props, onClick }: { props: any, onClick?: any }) { -function SquareAchievement ({ props }:{ props: any }) { + const navigate = useNavigate(); - return ( -
-
- + function handleClick() { + if (onClick) { + onClick(); + } else { + if (!props.game) { + return; -
- - achievement icon - -
- -
+ } + const gameId = props.game; + navigate(`/game/detail/${gameId}`); + } + } + return ( +
+
+ + achievement icon + +
); -}; - +} -export default SquareAchievement; \ No newline at end of file +export default SquareAchievement; diff --git a/app/frontend/src/Components/Application/Application.module.scss b/app/frontend/src/Components/Application/Application.module.scss new file mode 100644 index 00000000..83c4015a --- /dev/null +++ b/app/frontend/src/Components/Application/Application.module.scss @@ -0,0 +1,55 @@ + +@import '../../colors'; +.group { + display: flex; + flex-direction: column; + width: 100%; + background-color: $yellow; + border-radius: 0.5em; + gap: 10px; + padding: 10px; + border: 1px solid $yellow-dark-10; + .header { + height: 40px; + display: flex; + border-bottom: 2px solid $yellow-dark-40; + font-size: 18px; + + } + + .body{ + display: flex; + margin: 10px; + gap: 20px; + .img-container { + overflow: hidden; + border-radius: 0.5em; + + img { + object-fit: contain; + height: 100%; + width: 100%; + } + } + + .content { + display: flex; + flex-direction: column; + flex: 1; + .description { + display: flex; + flex-direction: column; + justify-content: center; + font-size: 15px; + margin: 10px; + flex: 1; + } + .footer { + display: flex; + justify-content: space-between; + align-items: flex-end; + } + } + + } +} \ No newline at end of file diff --git a/app/frontend/src/Components/Application/Application.tsx b/app/frontend/src/Components/Application/Application.tsx new file mode 100644 index 00000000..3c34c3da --- /dev/null +++ b/app/frontend/src/Components/Application/Application.tsx @@ -0,0 +1,78 @@ +import { TeamOutlined } from "@ant-design/icons"; +import styles from "./Application.module.scss"; +import { Button } from "antd"; +import { useMutation, useQueryClient } from "react-query"; +import { reviewApplication } from "../../Services/applications"; +import { NotificationUtil } from "../../Library/utils/notification"; +import { handleAxiosError } from "../../Library/utils/handleError"; + + +function Application({ application }: { application: any }) { + const queryClient = useQueryClient(); + + const { mutate: approve } = useMutation( + (applicationId: string) => reviewApplication(applicationId, "APPROVE"), + { + onSuccess() { + queryClient.invalidateQueries(["applications"]); + NotificationUtil.success("You successfully approved the application"); + }, + onError(error: any) { + handleAxiosError(error); + }, + } + ); + + const { mutate: reject } = useMutation( + (applicationId: string) => reviewApplication(applicationId, "REJECT"), + { + onSuccess() { + queryClient.invalidateQueries(["applications"]); + NotificationUtil.success("You successfully rejected the application"); + + }, + onError(error: any) { + handleAxiosError(error); + }, + } + ); + + return ( +
+
+
+ +
+ {application?.applicant.username} +
+
+
+
+
+
+
+ {application?.message} +
+
+
+
+ + +
+
+
+
+
+ ); +} + +export default Application; diff --git a/app/frontend/src/Components/Character/Character.module.scss b/app/frontend/src/Components/Character/Character.module.scss new file mode 100644 index 00000000..18f8d0f3 --- /dev/null +++ b/app/frontend/src/Components/Character/Character.module.scss @@ -0,0 +1,59 @@ +@import "../../colors"; + +.character-container { + width: 150px; + height: 150px; + margin-bottom: 20px; + box-shadow: 1px 1px 4px #000; + filter: saturate(0.8); + transition-property: transform, box-shadow, background-size, opacity, top, + left; + transition-timing-function: ease-out; + transition-duration: 0.3s; + background-position: center; + background-size: cover; + background-repeat: no-repeat; + overflow: hidden; + cursor: pointer; + background-color: $vanilla-light-80; +} + +.fade-container { + width: 100%; + height: 100%; + position: absolute; + transition-property: opacity, bottom; + transition-timing-function: ease-out; + transition-duration: 0.2s; + opacity: 0; + + .char-name { + width: 100%; + height: 50px; + position: absolute; + bottom: -20px; + left: 5px; + z-index: 3; + font-size: 18px; + line-height: 20px; + font-family: "Courier New", Courier, monospace; + font-weight: 600; + color: $color-text-light; + text-transform: uppercase; + letter-spacing: 1px; + padding: 8px; + padding-left: 0px; + text-shadow: 0px 0px 4px #000; + } +} + +.character-container:hover { + background-image: 150%; + transform: scale(1.1); + z-index: 10; +} + +.fade-container:hover { + opacity: 1; + bottom: 0; +} diff --git a/app/frontend/src/Components/Character/Character.tsx b/app/frontend/src/Components/Character/Character.tsx new file mode 100644 index 00000000..5dda7375 --- /dev/null +++ b/app/frontend/src/Components/Character/Character.tsx @@ -0,0 +1,42 @@ +import clsx from "clsx"; +import styles from "./Character.module.scss"; + +function Character({ + onClick, + name, + imgUrl, + className, +}: { + onClick?: () => void; + name: string; + imgUrl: string; + className?: string; +}) { + return ( +
+
onClick?.()} + > +
+
+
{name}
+
+
+
+
+ ); +} + +export default Character; diff --git a/app/frontend/src/Components/Character/CharacterDetails.module.scss b/app/frontend/src/Components/Character/CharacterDetails.module.scss new file mode 100644 index 00000000..25c7e46b --- /dev/null +++ b/app/frontend/src/Components/Character/CharacterDetails.module.scss @@ -0,0 +1,28 @@ +.modal-container { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + gap: 20px; + .image-container { + box-shadow: 1px 1px 4px #000; + width: 200px; + height: 200px; + max-height: 300px; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .description { + height: min-content; + max-width: 100%; + word-wrap: break-word; + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + } +} diff --git a/app/frontend/src/Components/Character/CharacterDetails.tsx b/app/frontend/src/Components/Character/CharacterDetails.tsx new file mode 100644 index 00000000..47208846 --- /dev/null +++ b/app/frontend/src/Components/Character/CharacterDetails.tsx @@ -0,0 +1,165 @@ +import { + Button, + ConfigProvider, + Descriptions, + DescriptionsProps, + Modal, +} from "antd"; +import { useEffect, useState } from "react"; +import Vibrant from "node-vibrant"; +import styles from "./CharacterDetails.module.scss"; +import Character from "./Character"; + +function CharacterDetails({ + character, + className, +}: { + character: any; + className?: string; +}) { + const [isModalOpen, setIsModalOpen] = useState(false); + const [palette, setPalette] = useState({} as any); + + const showModal = () => { + setIsModalOpen(true); + }; + + const handleOk = () => { + setIsModalOpen(false); + }; + + const handleCancel = () => { + setIsModalOpen(false); + }; + + const imageUrl = `${import.meta.env.VITE_APP_IMG_URL}${character?.icon}`; + + useEffect(() => { + const extractColors = async () => { + Vibrant.from(imageUrl) + .getPalette() + .then((palette) => { + // Access colors from the palette + if (palette) { + setPalette(palette); + } + }) + .catch((error) => { + console.error("Error extracting color palette:", error); + }); + }; + extractColors(); + }, [character?.icon]); + + const defaultItems: DescriptionsProps["items"] = [ + { + key: "type", + label: "Type", + children: character?.type || "Unknown", + }, + { + key: "race", + label: "Race", + children: character?.race || "Unknown", + }, + { + key: "gender", + label: "Gender", + children: character?.gender || "Unknown", + }, + { + key: "height", + label: "Height", + children: character?.height || "Unknown", + }, + { + key: "age", + label: "Age", + children: character?.age || "Unknown", + }, + { + key: "status", + label: "Status", + children: character?.status || "Unknown", + }, + { + key: "occupation", + label: "Occupation", + children: character?.occupation || "Unknown", + }, + { + key: "voiceActor", + label: "Voice Actor", + children: character?.voiceActor || "Unknown", + }, + ]; + + let borderedItems: DescriptionsProps["items"]; + + if (character?.customFields !== undefined) { + borderedItems = [ + ...defaultItems, + ...Object.keys(character?.customFields || {}).map((key: any) => { + return { + key: key, + label: key, + children: character?.customFields[key], + }; + }), + ]; + } else { + borderedItems = defaultItems; + } + + return ( + <> + + + +
+
+ character +
+
{character?.description}
+ +
+
+
+ + ); +} + +export default CharacterDetails; diff --git a/app/frontend/src/Components/Comment/Comment/Comment.module.scss b/app/frontend/src/Components/Comment/Comment/Comment.module.scss index b25e6289..5148ea2b 100644 --- a/app/frontend/src/Components/Comment/Comment/Comment.module.scss +++ b/app/frontend/src/Components/Comment/Comment/Comment.module.scss @@ -5,7 +5,6 @@ background-color: $celadon; border-radius: 0.5em; position: relative; - max-width: 70em; word-break: break-word; gap: 0.5em; diff --git a/app/frontend/src/Components/Comment/Comment/Comment.tsx b/app/frontend/src/Components/Comment/Comment/Comment.tsx index c8414723..75ee59ef 100644 --- a/app/frontend/src/Components/Comment/Comment/Comment.tsx +++ b/app/frontend/src/Components/Comment/Comment/Comment.tsx @@ -20,9 +20,10 @@ import { useQueryClient } from "react-query"; import { useState } from "react"; import ReplyForm from "../ReplyForm/ReplyForm"; import Reply from "../Reply/Reply"; -import { useNavigate } from "react-router-dom"; import CommentEditForm from "../CommentForm/CommentEditForm"; import { twj } from "tw-to-css"; +import { NotificationUtil } from "../../../Library/utils/notification"; +import clsx from "clsx"; function Comment({ comment, postId }: { comment: any; postId: string }) { const { upvote, downvote } = useVote({ @@ -32,13 +33,13 @@ function Comment({ comment, postId }: { comment: any; postId: string }) { }); const { user, isLoggedIn } = useAuth(); - const navigate = useNavigate(); const queryClient = useQueryClient(); const { mutate: removeComment } = useMutation( (id: string) => deleteComment(id), { onSuccess() { queryClient.invalidateQueries(["comments", postId]); + NotificationUtil.success("You successfully delete the comment."); }, onMutate(id: string) { queryClient.setQueriesData(["comments", postId], (prev: any) => { @@ -69,7 +70,7 @@ function Comment({ comment, postId }: { comment: any; postId: string }) { icon={} onClick={upvote} disabled={!isLoggedIn} - //className={clsx(post?.userVote === "UPVOTE" && styles.active)} + className={clsx(comment?.userVote === "UPVOTE" && styles.active)} />
{comment.overallVote}
@@ -80,7 +81,7 @@ function Comment({ comment, postId }: { comment: any; postId: string }) { icon={} onClick={downvote} disabled={!isLoggedIn} - //className={clsx(post?.userVote === "DOWNVOTE" && styles.active)} + className={clsx(comment?.userVote === "DOWNVOTE" && styles.active)} />
@@ -106,7 +107,7 @@ function Comment({ comment, postId }: { comment: any; postId: string }) { type="text" alt="report" /> - {(user.username === comment.commenter.username || user.isAdmin) && ( + {(user?.username === comment.commenter.username || user?.isAdmin) && (
)} - {user.id === comment.commenter.id && ( + {user?.id === comment.commenter.id && (
{reply.commentContent}
{reply.commenter.username} {reply.createdAt && formatDate(reply.createdAt)} - {user.username === reply.commenter.username && ( + {user?.username === reply.commenter.username && (
); diff --git a/app/frontend/src/Components/Forum/ForumPost/ForumPost.module.scss b/app/frontend/src/Components/Forum/ForumPost/ForumPost.module.scss index 1214c321..644d5953 100644 --- a/app/frontend/src/Components/Forum/ForumPost/ForumPost.module.scss +++ b/app/frontend/src/Components/Forum/ForumPost/ForumPost.module.scss @@ -3,6 +3,9 @@ .container { position: relative; background-color: $celadon; + &.group { + background-color: $yellow-light-40; + } border-radius: 0.5em; display: grid; gap: 0.5em; @@ -17,12 +20,20 @@ .imgConatiner { grid-area: i; - aspect-ratio: 1; padding: 0.5em; - img { + display: flex; + gap: 1em; + & > img { object-fit: contain; height: 100%; - width: 100%; + width: auto; + aspect-ratio: 1; + } + + .character { + height: 100%; + aspect-ratio: 1; + width: auto; } } @@ -33,9 +44,13 @@ gap: 0.5rem; grid-area: t; + overflow: hidden; + .title { font-weight: bold; font-size: 1.2em; + white-space: nowrap; + text-overflow: ellipsis; } } @@ -85,6 +100,9 @@ position: absolute; bottom: 0.5em; right: 0.5em; + display: flex; + gap: 1em; + align-items: center; } .edit { position: absolute; diff --git a/app/frontend/src/Components/Forum/ForumPost/ForumPost.tsx b/app/frontend/src/Components/Forum/ForumPost/ForumPost.tsx index 42c5989b..f6eb9b04 100644 --- a/app/frontend/src/Components/Forum/ForumPost/ForumPost.tsx +++ b/app/frontend/src/Components/Forum/ForumPost/ForumPost.tsx @@ -6,7 +6,6 @@ import { DownOutlined, EditOutlined, UpOutlined, - WarningOutlined, } from "@ant-design/icons"; import { useMutation } from "react-query"; import { deletePost } from "../../../Services/forum"; @@ -17,15 +16,27 @@ import { truncateWithEllipsis } from "../../../Library/utils/truncate"; import { useNavigate } from "react-router-dom"; import TagRenderer from "../../TagRenderer/TagRenderer"; import { twj } from "tw-to-css"; +import { NotificationUtil } from "../../../Library/utils/notification"; +import { handleAxiosError } from "../../../Library/utils/handleError"; +import SquareAchievement from "../../Achievement/SquareAchievement/SquareAchievement"; +import CharacterDetails from "../../Character/CharacterDetails"; function ForumPost({ post, forumId, redirect = "/", + gameId, + type = "STANDARD", + typeName, + typeId, }: { post: any; forumId: string; redirect?: string; + gameId?: string; + type?: "STANDARD" | "GROUP" | "GAME"; + typeName?: string; + typeId?: string; }) { const { user, isLoggedIn } = useAuth(); const navigate = useNavigate(); @@ -33,10 +44,10 @@ function ForumPost({ const isAdmin = user?.role === "ADMIN"; const deletePostMutation = useMutation(deletePost, { onSuccess: async () => { - alert("You successfully delete the post."); + NotificationUtil.success("You successfully delete the post."); }, - onError: () => { - alert("Something went wrong"); + onError: (error) => { + handleAxiosError(error); }, }); @@ -52,8 +63,11 @@ function ForumPost({ ["post", post.id], ], }); + const typeStyle = + type === "GAME" ? styles.game : type === "GROUP" ? styles.group : undefined; + return ( -
+
- {post.postImage && ( + {(post.postImage || post.achievement || post.character) && (
- + {post.postImage && ( + + )} + {post.achievement && } + + {post.character && ( + + )}
)} @@ -99,25 +123,41 @@ function ForumPost({
- {post.poster.username} + {post.poster?.username} {post.createdAt && formatDate(post.createdAt)} -
- + )} +
- {user?.id === post.poster.id && ( + {user?.id === post.poster?.id && (
+
+
diff --git a/app/frontend/src/Components/GameDetails/Review/Reviews.tsx b/app/frontend/src/Components/GameDetails/Review/Reviews.tsx index a8a0650b..7e67c57c 100644 --- a/app/frontend/src/Components/GameDetails/Review/Reviews.tsx +++ b/app/frontend/src/Components/GameDetails/Review/Reviews.tsx @@ -10,11 +10,12 @@ import { SortDescendingOutlined, } from "@ant-design/icons"; import { isEmpty } from "../../../Library/utils/isEmpty"; +import { useAuth } from "../../Hooks/useAuth"; function Reviews({ gameId }: { gameId: string }) { const [reviewedBy, setReviewedBy] = useState(); const [searchText, setSearchText] = useState(""); - + const { isLoggedIn } = useAuth(); const { Search } = Input; const sortOptions = [ @@ -62,7 +63,7 @@ function Reviews({ gameId }: { gameId: string }) { style={{ width: "200px" }} />
- + {isLoggedIn && } {!isEmpty(reviews) ? ( reviews ?.filter((review: any) => { diff --git a/app/frontend/src/Components/GameDetails/Summary/Summary.module.scss b/app/frontend/src/Components/GameDetails/Summary/Summary.module.scss index 8dd97fdc..7789acf4 100644 --- a/app/frontend/src/Components/GameDetails/Summary/Summary.module.scss +++ b/app/frontend/src/Components/GameDetails/Summary/Summary.module.scss @@ -48,10 +48,25 @@ white-space: pre-wrap; } - .row { - display: flex; - flex-direction: row; - align-items: center; - position: relative; - margin-left: 10px; - } +.row { + display: flex; + flex-wrap: wrap; + flex-direction: row; + align-items: center; + position: relative; + margin-left: 10px; + max-width: 100%; +} + +.infoContainer { + margin: 0.5rem; +} + +.charTitle { + font-size: 1.2em; + font-weight: bold; + margin-bottom: 0.5em; + margin-left: 0.5em; + margin-top: 1em; + color: $orange-dark-60; +} diff --git a/app/frontend/src/Components/GameDetails/Summary/Summary.tsx b/app/frontend/src/Components/GameDetails/Summary/Summary.tsx index 1a677d6a..c3d1bb4c 100644 --- a/app/frontend/src/Components/GameDetails/Summary/Summary.tsx +++ b/app/frontend/src/Components/GameDetails/Summary/Summary.tsx @@ -4,16 +4,114 @@ import styles from "./Summary.module.scss"; import { getAchievementByGame } from "../../../Services/achievement"; import Achievement from "../../Achievement/Achievement/Achievement"; import { useQuery } from "react-query"; +import { handleAxiosError } from "../../../Library/utils/handleError.ts"; +import { Recogito } from "@recogito/recogito-js"; - - +import "@recogito/recogito-js/dist/recogito.min.css"; +import { + createAnnotation, + deleteAnnotation, + updateAnnotation, +} from "../../../Services/annotation.ts"; +import { NotificationUtil } from "../../../Library/utils/notification.ts"; +import { useEffect, useState } from "react"; +import { useAuth } from "../../Hooks/useAuth.tsx"; +import { getCharacterByGame } from "../../../Services/character"; +import CharacterDetails from "../../Character/CharacterDetails"; function Summary({ game }: { game: any }) { + const { user } = useAuth(); + const [isAnnotationsApplied, setIsAnnotationsApplied] = useState(false); const { data: achievements, isLoading: isLoadingAchievements } = useQuery( ["achievements", game.id], () => getAchievementByGame({ gameId: game.id! }) ); - + + const pageUrl = window.location.href.replace("?back=/home", ""); + + const isAdmin = user?.role === "ADMIN"; + + const hideTagField = () => { + const tagField = document.querySelector(".r6o-widget.r6o-tag"); + + if (tagField) { + tagField.style.display = "none"; + } + }; + + useEffect(() => { + const textElement = document.querySelector("#textElement"); + + if (textElement) { + textElement.addEventListener("click", hideTagField); + } + }, [game]); + + const linkAnnotation = (elem: any) => { + if (elem && isAnnotationsApplied === false) { + const r = new Recogito({ + content: elem, + readOnly: !isAdmin, + }); + setIsAnnotationsApplied(true); + + r.loadAnnotations( + `${ + import.meta.env.VITE_APP_ANNOTATION_API_URL + }/annotation/get-source-annotations?source=${pageUrl}` + ) + .then(function (annotations) {}) + .catch((error) => { + if (error instanceof SyntaxError) { + return; + } + NotificationUtil.error("Error occurred while retrieving annotations"); + }); + + + r.on("createAnnotation", async (annotation: any, overrideId) => { + try { + annotation.target = { ...annotation.target, source: pageUrl }; + const newId = pageUrl + "/" + annotation.id.replace("#", ""); + annotation.id = newId; + overrideId(newId); + await createAnnotation(annotation); + NotificationUtil.success("You successfully create the annotation"); + } catch (error) { + handleAxiosError(error); + } + }); + + r.on("deleteAnnotation", async function (annotation: any) { + try { + const id = annotation.id; + await deleteAnnotation(id); + NotificationUtil.success("You successfully delete the annotation"); + } catch (error) { + handleAxiosError(error); + } + }); + + r.on("selectAnnotation", async function (annotation: any) { + hideTagField(); + }); + + r.on("updateAnnotation", async function (annotation, _previous) { + try { + annotation.target = { ...annotation.target, source: pageUrl }; + await updateAnnotation(annotation); + NotificationUtil.success("You successfully update the annotation"); + } catch (error) { + handleAxiosError(error); + } + }); + } + }; + const { data: characters, isLoading: isLoadingCharacters } = useQuery( + ["characters", game.id], + () => getCharacterByGame(game.id) + ); + return (
@@ -67,7 +165,9 @@ function Summary({ game }: { game: any }) { )}
- {game?.gameDescription} + linkAnnotation(elem)} id="textElement"> + {game?.gameDescription} +
{game.minSystemReq && (
@@ -76,24 +176,39 @@ function Summary({ game }: { game: any }) {
)} + {!isLoadingCharacters && characters.length > 0 && ( +
+
Characters
+
+ {characters.map( + (character: any) => + !character.isDeleted && ( +
+ +
+ ) + )} +
+
+ )} - {!isLoadingAchievements && achievements.length > 0 && -
-
Achievements
-
- { - achievements.map( - (achievement: any) => - !achievement.isDeleted && ( - - ) - )} + {!isLoadingAchievements && achievements.length > 0 && ( +
+
Achievements
+
+ {achievements.map( + (achievement: any) => + !achievement.isDeleted && ( +
+
+ +
+
+ ) + )} +
-
- } - - - + )}
); } diff --git a/app/frontend/src/Components/Groups/PrivateGroup.tsx b/app/frontend/src/Components/Groups/PrivateGroup.tsx index ccc5ad42..e9e13db7 100644 --- a/app/frontend/src/Components/Groups/PrivateGroup.tsx +++ b/app/frontend/src/Components/Groups/PrivateGroup.tsx @@ -8,6 +8,10 @@ import { useNavigate } from "react-router-dom"; function PrivateGroup({ group }: { group: any }) { const navigate = useNavigate(); + const handleClick = async () => { + navigate(`/group/apply/${group.id}`); + }; + return (
@@ -21,7 +25,13 @@ function PrivateGroup({ group }: { group: any }) {
- +
@@ -41,7 +51,7 @@ function PrivateGroup({ group }: { group: any }) { {group.userJoined ? ( ) : ( - + )}
- +
diff --git a/app/frontend/src/Components/Hooks/useAuth.tsx b/app/frontend/src/Components/Hooks/useAuth.tsx index 5eb60b9b..4dbe19a0 100644 --- a/app/frontend/src/Components/Hooks/useAuth.tsx +++ b/app/frontend/src/Components/Hooks/useAuth.tsx @@ -11,6 +11,7 @@ import { import { me } from "../../Services/me"; import { useQuery } from "react-query"; import { getProfile } from "../../Services/profile"; +import { useNavigate } from "react-router-dom"; type User = any; @@ -51,7 +52,7 @@ const useAuth = (): UseAuthProps => { function logOut() { Cookies.remove("token"); - location.reload(); + location.replace("/"); } const { data: profile, isLoading } = useQuery( @@ -68,13 +69,14 @@ const useAuth = (): UseAuthProps => { isLoggedIn: !!user, logOut, profile, - isLoading: !user || isLoading, + isLoading: isLoading, }; }; // AuthProvider component const AuthProvider = ({ children }: { children?: ReactNode }) => { const [user, setUser] = useState(null); // Initialize to fetch from local storage or server if needed + const [loading, setLoading] = useState(true); useEffect(() => { const token = Cookies.get("token"); @@ -82,7 +84,10 @@ const AuthProvider = ({ children }: { children?: ReactNode }) => { axios.defaults.headers.common["Authorization"] = `${token}`; me().then((res) => { setUser?.(res.data); + setLoading(false); }); + } else { + setLoading(false); } }, []); @@ -91,7 +96,11 @@ const AuthProvider = ({ children }: { children?: ReactNode }) => { setUser, }; - return {children}; + return ( + + {!loading && children} + + ); }; export { useAuth, AuthProvider }; diff --git a/app/frontend/src/Components/Hooks/useVote.tsx b/app/frontend/src/Components/Hooks/useVote.tsx index 8daa42f8..7ae0f676 100644 --- a/app/frontend/src/Components/Hooks/useVote.tsx +++ b/app/frontend/src/Components/Hooks/useVote.tsx @@ -17,6 +17,7 @@ export function useVote({ (choice: "UPVOTE" | "DOWNVOTE") => createVote({ voteType, typeId, choice }), { onSuccess() { + queryClient.invalidateQueries(["home"]); if (invalidateKey) { queryClient.invalidateQueries(invalidateKey); } diff --git a/app/frontend/src/Components/LastActivities/LastActivities.module.scss b/app/frontend/src/Components/LastActivities/LastActivities.module.scss new file mode 100644 index 00000000..03aec5b5 --- /dev/null +++ b/app/frontend/src/Components/LastActivities/LastActivities.module.scss @@ -0,0 +1,10 @@ +@import "../../colors"; +.container { + padding: 2rem; + display: flex; + flex-direction: column; + gap: 5px; + .sub-item { + width: 100%; + } +} diff --git a/app/frontend/src/Components/LastActivities/LastActivities.tsx b/app/frontend/src/Components/LastActivities/LastActivities.tsx new file mode 100644 index 00000000..cd30a1be --- /dev/null +++ b/app/frontend/src/Components/LastActivities/LastActivities.tsx @@ -0,0 +1,111 @@ +import { getActivities } from "../../Services/activities"; +import { useQuery } from "react-query"; +import styles from "./LastActivities.module.scss"; +import { + CommentOutlined, + DownCircleOutlined, + TeamOutlined, + UpCircleOutlined, +} from "@ant-design/icons"; +import { Card } from "antd"; +import { Link } from "react-router-dom"; + +function LastActivities() { + const activities = useQuery(["activites"], () => getActivities()); + + return ( +
+ {!activities.data ? ( +

No activities

+ ) : ( + activities.data.map((activity: any) => { + return ( +
+ {(() => { + if (activity.type === "COMMENT") { + return ( + } + size="small" + > + {`"${activity.description}"`} + + ); + } else if (activity.type === "REVIEW") { + return ( + + Details + + } + size="small" + > + {`"${activity.description}"`} + + ); + } else if (activity.type === "POST") { + return ( + + Details + + } + size="small" + > + {`"${activity.description}"`} + + ); + } else if (activity.type === "VOTE") { + return ( + + ) : ( + + ) + } + size="small" + > + {`"${activity.description}"`} + + ); + } else if (activity.type === "GROUP") { + return ( + } + size="small" + > + {`"${activity.description}"`} + + ); + } + // Add more else-if conditions as needed + return null; // Optional: Default case or no-op + })()} +
+ ); + }) + )} +
+ ); +} + +export default LastActivities; +// diff --git a/app/frontend/src/Components/MemberList/MemberList.module.scss b/app/frontend/src/Components/MemberList/MemberList.module.scss new file mode 100644 index 00000000..bffc843b --- /dev/null +++ b/app/frontend/src/Components/MemberList/MemberList.module.scss @@ -0,0 +1,5 @@ +@import "../../colors"; + +.username { + color: $orange; +} diff --git a/app/frontend/src/Components/MemberList/MemberList.tsx b/app/frontend/src/Components/MemberList/MemberList.tsx new file mode 100644 index 00000000..f4aef269 --- /dev/null +++ b/app/frontend/src/Components/MemberList/MemberList.tsx @@ -0,0 +1,50 @@ +import { Button, List, Select } from "antd"; +import styles from "./MemberList.module.scss"; +import { useMutation } from "react-query"; +import { banUserFromGroup } from "../../Services/group"; +import { NotificationUtil } from "../../Library/utils/notification"; +import { handleError } from "../../Library/utils/handleError"; +interface MemberListData { + id: string; + username: string; + photoUrl: string; +} + +function MemberList({ + data, + groupId, +}: { + data: MemberListData[]; + groupId: string; +}) { + const banUserMutation = useMutation(banUserFromGroup, { + onSuccess: async () => { + NotificationUtil.success("You successfully banned the user"); + }, + onError: (error) => { + handleError(error); + }, + }); + + const handleClick = (userId: string) => { + banUserMutation.mutate({ groupId, userId }); + }; + return ( + ( + + } + title={

{item.username}

} + /> + +
+ )} + /> + ); +} + +export default MemberList; diff --git a/app/frontend/src/Components/Notification/Notification.module.scss b/app/frontend/src/Components/Notification/Notification.module.scss new file mode 100644 index 00000000..d8f67c8b --- /dev/null +++ b/app/frontend/src/Components/Notification/Notification.module.scss @@ -0,0 +1,41 @@ +@import "../../colors"; + +.notification { + display: flex; + align-items: center; + padding: 10px; + border: 1px solid #ccc; + border-radius: 0.5rem; + transition-property: border-color height; + transition-duration: 0.2s; + margin: 5px; + height: 4em; + overflow: hidden; + background-color: $blue-green-light-50; + position: relative; + + .notification-content { + font-size: 15px; + } + .date { + color: $blue-green-dark-50; + font-size: 12px; + position: absolute; + bottom: -5px; + right: 5px; + opacity: 0; + } +} + +.notification.unread { + background-color: $blue-green-light-10; +} + +.notification:hover { + border-color: #007bff; /* Highlight border on hover */ + height: 6em; + opacity: 0.7; + .date { + opacity: 1; + } +} diff --git a/app/frontend/src/Components/Notification/Notification.tsx b/app/frontend/src/Components/Notification/Notification.tsx new file mode 100644 index 00000000..7cc75876 --- /dev/null +++ b/app/frontend/src/Components/Notification/Notification.tsx @@ -0,0 +1,41 @@ +import styles from "./Notification.module.scss"; +import { useNavigate } from "react-router-dom"; +import { getPost } from "../../Services/forum"; +import { formatDate } from "../../Library/utils/formatDate"; +import clsx from "clsx"; + +function Notification({ props }: { props: any }) { + const navigate = useNavigate(); + + async function handleClick() { + if (props.parentType !== "ACHIEVEMENT") { + if (props.parentType === "POST") { + const post = await getPost(props.parent); + navigate( + `/forum/detail/${post.forumId}/${props.parent}?back=/notifications` + ); + } else if (props.parentType === "COMMENT") { + const post = await getPost(props.parent); + navigate( + `/forum/detail/${post.forumId}/${props.parent}?back=/notifications` + ); + } else if (props.parentType === "GROUP") { + navigate(`/group/detail/${props.parent}`); + } else if (props.parentType === "GROUP_APPLICATION") { + navigate(`/group/review-application/${props.parent}`); + } + } + } + + return ( +
+

{props.message}

+

{formatDate(props.createdAt)}

+
+ ); +} + +export default Notification; diff --git a/app/frontend/src/Components/Profile/EditProfile.tsx b/app/frontend/src/Components/Profile/EditProfile.tsx index 1dabb2cf..7e6025ad 100644 --- a/app/frontend/src/Components/Profile/EditProfile.tsx +++ b/app/frontend/src/Components/Profile/EditProfile.tsx @@ -5,7 +5,7 @@ import { editProfile } from "../../Services/profile"; import UploadArea from "../UploadArea/UploadArea"; import { useForm } from "antd/es/form/Form"; import { useMutation, useQueryClient } from "react-query"; -import { message } from "antd"; +import { handleAxiosError } from "../../Library/utils/handleError"; function EditProfile({ profile }: { profile: any }) { const profileId = profile.id; @@ -19,7 +19,7 @@ function EditProfile({ profile }: { profile: any }) { form.setFieldsValue(profile); form.setFieldValue("username", profile?.user?.username); setImageUrl(profile?.profilePhoto); - }, [profile]); + }, [profile, form]); const showModal = () => { setOpen(true); @@ -28,13 +28,16 @@ function EditProfile({ profile }: { profile: any }) { const { mutate: edit } = useMutation( (data: any) => editProfile(data, profileId), { - onSuccess() { + onSuccess(_, data: any) { + if (data.username) { + location.reload(); + } queryClient.invalidateQueries(["profile", profile.user.id]); setConfirmLoading(false); setOpen(false); }, onError(error: any) { - message.error(error.message); + handleAxiosError(error); setConfirmLoading(false); }, } @@ -43,7 +46,7 @@ function EditProfile({ profile }: { profile: any }) { const handleConfirm = async (data: any) => { setConfirmLoading(true); const newdata = { ...data, ...{ profilePhoto: imageUrl } }; - console.log(newdata); + edit(newdata); }; @@ -57,7 +60,12 @@ function EditProfile({ profile }: { profile: any }) { return ( <> - +
+ +
+
+ {!expand + ? achievements.map((a) => ) + : achievements.map((a) => )} +
+
+ ); +} + +export default ProfileAchievements; diff --git a/app/frontend/src/Components/PromotedEntities/PromotedEntities.module.scss b/app/frontend/src/Components/PromotedEntities/PromotedEntities.module.scss new file mode 100644 index 00000000..d892f69e --- /dev/null +++ b/app/frontend/src/Components/PromotedEntities/PromotedEntities.module.scss @@ -0,0 +1,86 @@ +@import "../../colors"; + +.container { + background-color: $violet-light-80; + border-radius: 0.5em; + overflow: hidden; + border: 3px solid $yellow; + justify-content: center; + align-items: center; + width: 100%; + display: flex; + justify-content: center; +} + +@media only screen and (max-width: 976px) { + .container { + flex: 0 1 100%; + } +} + +@media only screen and (min-width: 976px) { + .container { + flex: 0 1 calc(100% * (1 / 2) - 1rem); + } +} + +.header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + background-color: $violet; + cursor: pointer; + h1 { + margin-left: 10px; + } +} + +.descriptionContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.2rem; +} + +.description { + margin-left: 1rem; + margin-top: 0; + text-wrap: wrap; +} + +.icon { + margin-right: 1rem; + border-radius: 10px; + width: 240px; + height: 200px; + cursor: pointer; +} + +.content { + display: flex; + padding: 1rem; + text-wrap: wrap; + position: relative; + align-items: center; +} + +.button { + position: absolute; + + right: 0.5em; + bottom: 0.5em; +} + +.promotion { + color: $orange-dark-60; +} + +.crown { + color: $yellow-dark-20; + font-size: 1.5rem; + margin-left: 20px; + margin-right: 20px; +} diff --git a/app/frontend/src/Components/PromotedEntities/PromotedEntities.tsx b/app/frontend/src/Components/PromotedEntities/PromotedEntities.tsx new file mode 100644 index 00000000..8b1f85d1 --- /dev/null +++ b/app/frontend/src/Components/PromotedEntities/PromotedEntities.tsx @@ -0,0 +1,48 @@ +import { CrownFilled, TeamOutlined, UserOutlined } from "@ant-design/icons"; +import styles from "./PromotedEntities.module.scss"; +import { Button, Carousel, Tooltip, message } from "antd"; + +import { useNavigate } from "react-router-dom"; +import { truncateWithEllipsis } from "../../Library/utils/truncate"; +import { useElementSize } from "usehooks-ts"; + +function PromotedEntities({ games }: { games: any[] }) { + const navigate = useNavigate(); + const [containerRef, { width }] = useElementSize(); + + return ( +
+ + {games.map((game: any) => ( +
+
navigate(`/game/detail/${game.id}`)} + > +

{game.gameName}

+ + + +
+ +
+ {game.gameName} navigate(`/game/detail/${game.id}`)} + > +
+

+ {truncateWithEllipsis(game.gameDescription, 300)} +

+
+
+
+ ))} +
+
+ ); +} + +export default PromotedEntities; diff --git a/app/frontend/src/Components/PromotedEntities/PromotedPost.module.scss b/app/frontend/src/Components/PromotedEntities/PromotedPost.module.scss new file mode 100644 index 00000000..cb6ae00a --- /dev/null +++ b/app/frontend/src/Components/PromotedEntities/PromotedPost.module.scss @@ -0,0 +1,118 @@ +@import "../../colors"; + +.container { + position: relative; + background-color: $celadon; + &.group { + background-color: $yellow-light-40; + } + border-radius: 0.5em; + display: grid; + gap: 0.5em; + overflow: hidden; + color: $color-text; + grid-template-rows: 30px 55px 35px; + grid-template-columns: 60px min-content 1fr; + grid-template-areas: + "v i t" + "v i c" + "v m m"; + + .imgConatiner { + grid-area: i; + padding: 0.5em; + display: flex; + gap: 1em; + & > img { + object-fit: contain; + height: 100%; + width: auto; + aspect-ratio: 1; + } + + .character { + height: 100%; + aspect-ratio: 1; + width: auto; + } + } + + .titleContainer { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + grid-area: t; + + overflow: hidden; + + .title { + font-weight: bold; + font-size: 1.2em; + white-space: nowrap; + text-overflow: ellipsis; + } + } + + .content { + grid-area: c; + overflow: hidden; + opacity: 80%; + padding-right: 2.5em; + } + + .vote { + grid-area: v; + grid-template-rows: repeat(3, 1fr); + justify-items: center; + align-items: center; + display: grid; + color: white; + background-color: $orange-light-40; + button { + cursor: pointer; + background-color: $orange-dark-40; + color: white; + + &.active { + background-color: $orange; + } + + &:disabled { + opacity: 40%; + } + } + } + + .meta { + display: flex; + align-items: end; + gap: 1em; + padding: 0.5em; + + grid-area: m; + & > span { + font-size: 0.8em; + opacity: 80%; + } + } + .readMore { + position: absolute; + bottom: 0.5em; + right: 0.5em; + display: flex; + gap: 1em; + align-items: center; + } + .edit { + position: absolute; + top: 0.5em; + right: 0.5em; + } +} + +.crown { + color: $yellow-dark-20; + margin-left: 20px; + margin-right: 20px; + } diff --git a/app/frontend/src/Components/PromotedEntities/PromotedPosts.tsx b/app/frontend/src/Components/PromotedEntities/PromotedPosts.tsx new file mode 100644 index 00000000..efe6bc51 --- /dev/null +++ b/app/frontend/src/Components/PromotedEntities/PromotedPosts.tsx @@ -0,0 +1,176 @@ +import { Button, Tooltip } from "antd"; +import { formatDate } from "../../Library/utils/formatDate"; +import styles from "./PromotedPost.module.scss"; +import { + CrownFilled, + DeleteOutlined, + DownOutlined, + EditOutlined, + UpOutlined, +} from "@ant-design/icons"; +import { useMutation } from "react-query"; +import { deletePost } from "../../Services/forum"; +import { useAuth } from "../Hooks/useAuth"; +import { useVote } from "../Hooks/useVote"; +import clsx from "clsx"; +import { truncateWithEllipsis } from "../../Library/utils/truncate"; +import { useNavigate } from "react-router-dom"; +import TagRenderer from "../TagRenderer/TagRenderer"; +import { twj } from "tw-to-css"; +import { NotificationUtil } from "../../Library/utils/notification"; +import { handleAxiosError } from "../../Library/utils/handleError"; +import SquareAchievement from "../Achievement/SquareAchievement/SquareAchievement"; +import CharacterDetails from "../Character/CharacterDetails"; + +function PromotedForumPost({ + post, + forumId, + redirect = "/", + gameId, + type = "STANDARD", + typeName, + typeId, +}: { + post: any; + forumId: string; + redirect?: string; + gameId?: string; + type?: "STANDARD" | "GROUP" | "GAME"; + typeName?: string; + typeId?: string; +}) { + const { user, isLoggedIn } = useAuth(); + const navigate = useNavigate(); + + const isAdmin = user?.role === "ADMIN"; + const deletePostMutation = useMutation(deletePost, { + onSuccess: async () => { + NotificationUtil.success("You successfully delete the post."); + }, + onError: (error) => { + handleAxiosError(error); + }, + }); + + const handleDelete = () => { + deletePostMutation.mutate(post.id); + }; + + const { upvote, downvote } = useVote({ + voteType: "POST", + typeId: post.id, + invalidateKeys: [ + ["forum", forumId], + ["post", post.id], + ], + }); + const typeStyle = + type === "GAME" ? styles.game : type === "GROUP" ? styles.group : undefined; + + return ( +
+
+
+ + {(post.postImage || post.achievement || post.character) && ( +
+ {post.postImage && ( + + )} + {post.achievement && } + + {post.character && ( + + )} +
+ )} + +
+
{post.title}
+ {isAdmin && ( + + )} + + + + + + +
+ +
+ {truncateWithEllipsis(post.postContent, 300)} +
+ +
+ {post.poster?.username} + {post.createdAt && formatDate(post.createdAt)} +
+
+ {type !== "STANDARD" && ( + + )} + +
+ {user?.id === post.poster?.id && ( +
+ +
+ )} +
+ ); +} + +export default PromotedForumPost; diff --git a/app/frontend/src/Components/Providers/AntdConfigProvider.tsx b/app/frontend/src/Components/Providers/AntdConfigProvider.tsx index 74c9195b..ef93e61d 100644 --- a/app/frontend/src/Components/Providers/AntdConfigProvider.tsx +++ b/app/frontend/src/Components/Providers/AntdConfigProvider.tsx @@ -13,6 +13,8 @@ function AntdConfigProvider({ children }: { children: ReactNode }) { colorBgBase: getThemeColor("color-background"), colorTextBase: getThemeColor("color-text"), colorPrimary: getThemeColor("color-primary"), + colorBgContainer: "#fefdf5", + colorBorder: "#ddd68e", }, }; return {children}; diff --git a/app/frontend/src/Components/ReccomendationCarousel/RecommendationCarousel.module.scss b/app/frontend/src/Components/ReccomendationCarousel/RecommendationCarousel.module.scss new file mode 100644 index 00000000..f68d48f7 --- /dev/null +++ b/app/frontend/src/Components/ReccomendationCarousel/RecommendationCarousel.module.scss @@ -0,0 +1,18 @@ +@import "../../colors"; + +.recommendationCarouselContainer { + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + background-color: $prussian-blue; + padding: 0.5em; + border-radius: 0.5em; + height: auto; + + h2 { + color: $blue-green-light-80; + margin: 0; + margin-bottom: 0.75em; + } +} diff --git a/app/frontend/src/Components/ReccomendationCarousel/RecommendationCarousel.tsx b/app/frontend/src/Components/ReccomendationCarousel/RecommendationCarousel.tsx new file mode 100644 index 00000000..790e844d --- /dev/null +++ b/app/frontend/src/Components/ReccomendationCarousel/RecommendationCarousel.tsx @@ -0,0 +1,47 @@ +import React, { useRef } from "react"; +import { Carousel } from "antd"; +import { useElementSize } from "usehooks-ts"; +import styles from "./RecommendationCarousel.module.scss"; +import RecommendationItem from "./RecommendationItem/RecommendationItem"; +import { PacmanLoader } from "react-spinners"; +import { twj } from "tw-to-css"; + +function RecommendationCarousel({ + items, + title, + loading, +}: { + items: { + image: string; + name: string; + link: string; + showName?: boolean; + }[]; + title?: string; + loading?: boolean; +}) { + const [containerRef, { width }] = useElementSize(); + + return ( +
+ {title &&

{title}

} + {!loading ? ( + + {items?.map((item, index) => ( + + ))} + + ) : ( +
+ +
+ )} +
+ ); +} + +export default RecommendationCarousel; diff --git a/app/frontend/src/Components/ReccomendationCarousel/RecommendationItem/RecommendationItem.module.scss b/app/frontend/src/Components/ReccomendationCarousel/RecommendationItem/RecommendationItem.module.scss new file mode 100644 index 00000000..1dd05cfa --- /dev/null +++ b/app/frontend/src/Components/ReccomendationCarousel/RecommendationItem/RecommendationItem.module.scss @@ -0,0 +1,78 @@ +@import "../../../colors"; + +.container { + height: 150px; + width: 150px; + padding: 0.5em; + background-color: $blue-green-dark-40; + margin-bottom: 2em; + border-radius: 0.5em; + position: relative; + display: block; + img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 0.5em; + overflow: hidden; + } + + .index { + font-size: 2em; + font-weight: bold; + color: $blue-green-light-90; + text-shadow: 2px 0 $blue-green, -2px 0 $blue-green, 0 2px $blue-green, + 0 -2px $blue-green, 1px 1px $blue-green, -1px -1px $blue-green, + 1px -1px $blue-green, -1px 1px $blue-green; + + position: absolute; + top: 0em; + left: 0.5em; + } + + .showName { + font-size: 1.2em; + font-weight: bold; + color: $blue-green-light-90; + text-shadow: 2px 0 $blue-green, -2px 0 $blue-green, 0 2px $blue-green, + 0 -2px $blue-green, 1px 1px $blue-green, -1px -1px $blue-green, + 1px -1px $blue-green, -1px 1px $blue-green; + + position: absolute; + bottom: 0.5em; + left: 0.5em; + } + + .overlay { + background-color: #00000088; + position: absolute; + inset: 0.5em; + border-radius: 0.5em; + opacity: 0%; + transition: opacity 200ms; + color: white; + font-size: 1.1em; + font-weight: bold; + + .icon { + top: 0.5em; + right: 0.5em; + position: absolute; + font-size: 1.1em; + } + + .name { + bottom: 0; + right: 0; + left: 0; + padding: 0.5em; + position: absolute; + } + } + + &:hover { + .overlay { + opacity: 100%; + } + } +} diff --git a/app/frontend/src/Components/ReccomendationCarousel/RecommendationItem/RecommendationItem.tsx b/app/frontend/src/Components/ReccomendationCarousel/RecommendationItem/RecommendationItem.tsx new file mode 100644 index 00000000..27174e3c --- /dev/null +++ b/app/frontend/src/Components/ReccomendationCarousel/RecommendationItem/RecommendationItem.tsx @@ -0,0 +1,48 @@ +import { ArrowRightOutlined, ExportOutlined } from "@ant-design/icons"; +import styles from "./RecommendationItem.module.scss"; +import { Link } from "react-router-dom"; + +function RecommendationItem({ + image, + name, + link, + index, + showName, +}: { + image: string; + name: string; + link: string; + index?: number; + showName?: boolean; +}) { + return ( + + {index !== undefined && {index}} + {image ? ( + {name} + ): + ({name})} + {showName && {name}} +
+ {!showName && {name}}{" "} +
+ +
+
+ + ); +} + +export default RecommendationItem; diff --git a/app/frontend/src/Layout/SideBar/SideBar.tsx b/app/frontend/src/Layout/SideBar/SideBar.tsx index 270ff15d..8e1e6bca 100644 --- a/app/frontend/src/Layout/SideBar/SideBar.tsx +++ b/app/frontend/src/Layout/SideBar/SideBar.tsx @@ -4,6 +4,7 @@ import { MenuFoldOutlined, MenuUnfoldOutlined, AppstoreOutlined, + BellOutlined } from "@ant-design/icons"; import type { MenuProps } from "antd"; import { ConfigProvider, Menu } from "antd"; @@ -12,6 +13,8 @@ import ProfileIcon from "../../Components/Icons/ProfileIcon"; import { clsx } from "clsx"; import { getThemeColor } from "../../Components/Providers/AntdConfigProvider"; import { useAuth } from "../../Components/Hooks/useAuth"; +import { useNavigate } from "react-router-dom"; +import { twj } from "tw-to-css"; type MenuItem = Required["items"][number]; @@ -31,25 +34,47 @@ function createItem( } as MenuItem; } -const items: MenuItem[] = [ - createItem("My Games", "sub1", , [ - createItem("Minecraft", "5"), - createItem("Rounds", "6"), - createItem("Dota", "7"), - createItem("Stardoll", "8"), - ]), - - createItem("My Groups", "sub2", , [ - createItem("RoundsAndRounds", "9"), - createItem("D0TA", "10"), - ]), -]; - function SideBar() { const [collapsed, setCollapsed] = useState(false); + const navigate = useNavigate(); + + const { user, isLoggedIn, profile } = useAuth(); + + const handleClick = (e: any) => { + if(e.key[0] === "game"){ + navigate(`/game/detail/${e.key[1]}`); + }else if(e.key[0] === "group"){ + navigate(`/group/detail/${e.key[1]}`); + }else if(e.key[0] === "notification"){ + navigate(`/notifications`); + } + } + - const { user, isLoggedIn } = useAuth(); + const items: MenuItem[] = [ + createItem( + "My Games", + "game", + , + profile?.games + .slice(0, 5) + .map((game: any) => createItem(game.gameName, game.id)) + ), + createItem( + "My Groups", + "group", + , + profile?.groups + .slice(0, 5) + .map((group: any) => createItem(group.title, group.id)) + ), + createItem( + "Notifications", + "notification", + , + ) + ]; return (
- {isLoggedIn ? ( - - ) : ( + {!isLoggedIn ? ( + ) : profile && profile.profilePhoto ? ( + + ) : ( + )}
- {!collapsed && isLoggedIn &&
{user.username}
} + {!collapsed && isLoggedIn && ( +
{user.username}
+ )} {!collapsed && !isLoggedIn &&
Game Guru
} {isLoggedIn && ( { + if(keyPath[1] === "game" || keyPath[1] === "group"){ + navigate(`${keyPath[1]}/detail/${keyPath[0]}`) + }else if(keyPath[0] === "notification"){ + navigate(`/notifications`) + } + } + } /> )}
diff --git a/app/frontend/src/Layout/TopBar/TopBar.tsx b/app/frontend/src/Layout/TopBar/TopBar.tsx index 96af2a57..654f3ef6 100644 --- a/app/frontend/src/Layout/TopBar/TopBar.tsx +++ b/app/frontend/src/Layout/TopBar/TopBar.tsx @@ -94,7 +94,6 @@ function TopBar() { { diff --git a/app/frontend/src/Library/utils/handleError.ts b/app/frontend/src/Library/utils/handleError.ts index 26e55298..29264c55 100644 --- a/app/frontend/src/Library/utils/handleError.ts +++ b/app/frontend/src/Library/utils/handleError.ts @@ -1,15 +1,30 @@ -import { message } from "antd"; +import { NotificationUtil } from "./notification"; export function handleError(error: any) { { let text; - if (typeof error === "string") { - text == error; + if (typeof error?.response?.data === "string") { + text = error?.response?.data; } else { text = (error as Error).message ?? `Unknown Error: ${JSON.stringify(error)}`; } - message.error(text); - console.error(error as Error); + NotificationUtil.error(text); } } + +export function handleAxiosError(error: any) { + { + const errorMessage = error?.response?.data; + + NotificationUtil.error( + errorMessage && !isObject(errorMessage) + ? errorMessage + : `An error occurred` + ); + } +} + +function isObject(variable: any) { + return typeof variable === "object" && variable !== null; +} diff --git a/app/frontend/src/Library/utils/notification.ts b/app/frontend/src/Library/utils/notification.ts new file mode 100644 index 00000000..11110ddd --- /dev/null +++ b/app/frontend/src/Library/utils/notification.ts @@ -0,0 +1,19 @@ +import { toast, TypeOptions } from "react-toastify"; + +export class NotificationUtil { + static error(message: string): void { + NotificationUtil.notify(message, "error"); + } + + static success(message: string): void { + NotificationUtil.notify(message, "success"); + } + + static warn(message: string): void { + NotificationUtil.notify(message, "warning"); + } + + private static notify(message: string, type: TypeOptions) { + toast[type](message); + } +} diff --git a/app/frontend/src/Pages/Achievement/Achievements.tsx b/app/frontend/src/Pages/Achievement/Achievements.tsx index c1fdc7c6..95a42d29 100644 --- a/app/frontend/src/Pages/Achievement/Achievements.tsx +++ b/app/frontend/src/Pages/Achievement/Achievements.tsx @@ -2,28 +2,42 @@ import { useQuery } from "react-query"; import { getAchievementByGame } from "../../Services/achievement"; import Achievement from "../../Components/Achievement/Achievement/Achievement"; import SquareAchievement from "../../Components/Achievement/SquareAchievement/SquareAchievement"; +import PromotedEntities from "../../Components/PromotedEntities/PromotedEntities"; +import { getGames } from "../../Services/games"; +import { useState } from "react"; function Achievements(){ const gameId:string = "841cbf45-90cc-47b7-a763-fa3a18218bf9" - const { data: achievements, isLoading: isLoadingAchievements } = useQuery( - ["achievements", gameId], - () => getAchievementByGame({ gameId: gameId! }) + const { data: games, isLoading: isLoadingGames } = useQuery( + ["Games", gameId], + () => getGames(), + { + onSuccess: (data) => { + // Check if the data is an array and has at least two elements + if (Array.isArray(data) && data.length >= 2) { + // Update promotedEntities with the first two elements of the data array + setPromotedEntities(data.slice(0, 2)); + } + }, + } ); - - + + const [promotedEntities, setPromotedEntities] = useState([]); return(
- {!isLoadingAchievements && - achievements.map( - (achievement: any) => - !achievement.isDeleted && ( - - ) - )} + {!isLoadingGames && + + +
+ +
+ + }
+ ) } diff --git a/app/frontend/src/Pages/Admin/Achievement/CreateAchievement/CreateAchievement.module.scss b/app/frontend/src/Pages/Admin/Achievement/CreateAchievement/CreateAchievement.module.scss index 7320979c..5c5d691c 100644 --- a/app/frontend/src/Pages/Admin/Achievement/CreateAchievement/CreateAchievement.module.scss +++ b/app/frontend/src/Pages/Admin/Achievement/CreateAchievement/CreateAchievement.module.scss @@ -4,7 +4,7 @@ padding: 4rem; .header { - color: $blue-green; + color: green; } .form { @@ -12,15 +12,12 @@ } .select { - width: 100%; + width: 50%; + margin: 2rem 0; } .input { margin-bottom: 2rem; - &WithMargin { - margin-bottom: 2rem; - margin-top: 2rem; - } } .colorHeader { color: #6ea2bb; diff --git a/app/frontend/src/Pages/Admin/Achievement/CreateAchievement/CreateAchievement.tsx b/app/frontend/src/Pages/Admin/Achievement/CreateAchievement/CreateAchievement.tsx index 9f4193c9..d5fe8413 100644 --- a/app/frontend/src/Pages/Admin/Achievement/CreateAchievement/CreateAchievement.tsx +++ b/app/frontend/src/Pages/Admin/Achievement/CreateAchievement/CreateAchievement.tsx @@ -7,6 +7,9 @@ import SingleSelect from "../../../../Components/SingleSelect/SingleSelect"; import { InboxOutlined } from "@ant-design/icons"; import { uploadImage } from "../../../../Services/image"; import { createAchievement } from "../../../../Services/achievement"; +import { getGames } from "../../../../Services/games"; +import { NotificationUtil } from "../../../../Library/utils/notification"; +import { handleError } from "../../../../Library/utils/handleError"; function CreateAchievement() { const ACHIEVEMENT_TYPES = ["GAME", "META"]; @@ -21,20 +24,26 @@ function CreateAchievement() { const createAchievementMutation = useMutation(createAchievement, { onSuccess: async () => { - alert("You successfully create achievement."); + NotificationUtil.success("You successfully create achievement."); }, - onError: () => { - alert("Something went wrong"); + onError: (error) => { + handleError(error); }, }); - const onChange = (_filterKey: string, value: string[] | string) => { + const { data: games } = useQuery(["games"], () => getGames()); + + const onChangeType = (_filterKey: string, value: string) => { setType(value as string); }; + const onChangeGame = (_filterKey: string, value: string) => { + setGame(value as string); + }; + const uploadImageMutation = useMutation(uploadImage, { - onError: () => { - alert("We cannot upload the image"); + onError: (error) => { + handleError(error); }, }); const handleClick = async () => { @@ -61,9 +70,9 @@ function CreateAchievement() { return; } - fileList.map((file) => { + fileList?.map((file) => { if (file.type.indexOf("image") === -1) { - alert("You can only upload image files!"); + NotificationUtil.error("You can only upload image files!"); setFileList([]); } else { setFileList([file]); @@ -93,7 +102,7 @@ function CreateAchievement() { filterKey="type" elements={ACHIEVEMENT_TYPES} reset={false} - onChange={onChange} + onChange={onChangeType} >

Achievement Icon

@@ -112,12 +121,14 @@ function CreateAchievement() {

You can only upload one image file.

- setGame(event.target.value)} - /> + game.gameName)} + onChange={onChangeGame} + reset={false} + > +

+
+
+ ); +} + +export default DeleteAchievement; diff --git a/app/frontend/src/Pages/Admin/Character/CreateCharacter/CreateCharacter.module.scss b/app/frontend/src/Pages/Admin/Character/CreateCharacter/CreateCharacter.module.scss new file mode 100644 index 00000000..ba4f7345 --- /dev/null +++ b/app/frontend/src/Pages/Admin/Character/CreateCharacter/CreateCharacter.module.scss @@ -0,0 +1,33 @@ +@import "../../../../colors"; + +.container { + padding: 4rem; + + .header { + color: green; + } + + .form { + width: 40rem; + } + + .select { + width: 100%; + } + + .input { + margin-bottom: 2rem; + } + .colorHeader { + color: #6ea2bb; + margin: 0 0 0.5rem 0; + } + + .button { + width: 25%; + } + + .picker { + margin-bottom: 2rem; + } +} diff --git a/app/frontend/src/Pages/Admin/Character/CreateCharacter/CreateCharacter.tsx b/app/frontend/src/Pages/Admin/Character/CreateCharacter/CreateCharacter.tsx new file mode 100644 index 00000000..a674c647 --- /dev/null +++ b/app/frontend/src/Pages/Admin/Character/CreateCharacter/CreateCharacter.tsx @@ -0,0 +1,212 @@ +import { useState } from "react"; +import { useMutation, useQuery } from "react-query"; +import styles from "./CreateCharacter.module.scss"; +import "react-datepicker/dist/react-datepicker.css"; +import { Button, Input, Upload } from "antd"; +import MultipleSelect from "../../../../Components/MultipleSelect/MultipleSelect"; +import { InboxOutlined } from "@ant-design/icons"; +import { uploadImage } from "../../../../Services/image"; +import { NotificationUtil } from "../../../../Library/utils/notification"; +import { handleAxiosError } from "../../../../Library/utils/handleError"; +import { createGame, getGames } from "../../../../Services/games"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import { createCharacter } from "../../../../Services/character"; +import TextArea from "antd/es/input/TextArea"; + +function CreateCharacter() { + const [name, setName] = useState(""); + const [type, setType] = useState(""); + const [gender, setGender] = useState(""); + const [race, setRace] = useState(""); + const [status, setStatus] = useState(""); + const [occupation, setOccupation] = useState(""); + const [birthDate, setBirthDate] = useState(new Date()); + const [voiceActor, setVoiceActor] = useState(""); + const [height, setHeight] = useState(""); + const [age, setAge] = useState(""); + const [description, setDescription] = useState(""); + const [characterGames, setCharacterGames] = useState([]); + + const [fileList, setFileList] = useState([]); + + const { data: games } = useQuery(["games"], () => getGames()); + + const { Dragger } = Upload; + + const addCharacterMutation = useMutation(createCharacter, { + onSuccess: async () => { + NotificationUtil.success("You successfully create character."); + }, + onError: (error) => { + handleAxiosError(error); + }, + }); + + const uploadImageMutation = useMutation( + (i: any) => uploadImage(i, "character-icons"), + { + onError: (error) => { + handleAxiosError(error); + }, + } + ); + const handleClick = async () => { + const gameIcon = await uploadImageMutation.mutateAsync( + fileList[0].originFileObj + ); + const gameIds = characterGames.map((gameName) => { + return games.find((game) => game.gameName === gameName).id; + }); + + addCharacterMutation.mutate({ + name, + type, + gender, + race, + status, + occupation, + birthDate: birthDate as Date, + voiceActor, + height, + age, + description, + games: gameIds, + icon: gameIcon, + }); + }; + + const handleChange = async (info: any) => { + let fileList = [...info.fileList]; + + fileList = fileList.slice(-1); + + if (fileList.length === 0) { + setFileList([]); + return; + } + + fileList.map((file) => { + if (file.type.indexOf("image") === -1) { + NotificationUtil.error("You can only upload image files!"); + setFileList([]); + } else { + setFileList([file]); + } + }); + }; + + const onGameChange = async (_filterKey: string, value: string[]) => { + setCharacterGames(value); + }; + + return ( +
+

Create Character

+ +
+ setName(event.target.value)} + /> +

Character Image

+ false} + onChange={handleChange} + className={styles.dragger} + > +

+ +

+

+ Click or drag an image file to this area to upload +

+

You can only upload one image file.

+
+

+ {games?.length > 0 && ( + game.gameName)} + reset={false} + onChange={onGameChange} + > + )} +

+

Birth Date

+ setBirthDate(date)} + className={styles.datePicker} + /> +

+

+

+ setType(event.target.value)} + /> + setGender(event.target.value)} + /> + setRace(event.target.value)} + /> + setStatus(event.target.value)} + /> + setOccupation(event.target.value)} + /> + + setVoiceActor(event.target.value)} + /> + setHeight(event.target.value)} + /> + setAge(event.target.value)} + /> +