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 ada83eee..e9331ac2 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 @@ -6,6 +6,7 @@ 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.validation.annotation.AdminRequired; import com.app.gamereview.util.validation.annotation.AuthorizationRequired; @@ -17,6 +18,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.HashSet; import java.util.List; @RestController @@ -104,4 +106,12 @@ public ResponseEntity deleteGame(@RequestParam String id, @RequestHeade Boolean isDeleted = gameService.deleteGame(id); return ResponseEntity.ok(isDeleted); } + + @AuthorizationRequired + @GetMapping("/get-recommendations") + public ResponseEntity> getRecommendedGames(@RequestHeader String Authorization, HttpServletRequest request) { + User user = (User) request.getAttribute("authenticatedUser"); + List games = gameService.getRecommendedGames(user); + return ResponseEntity.ok(games); + } } 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 d7815803..20dbfe5e 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 @@ -5,6 +5,7 @@ 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; @@ -182,4 +183,12 @@ public ResponseEntity> listApplications(@Reque List applications = groupService.listApplications(groupId, user); return ResponseEntity.ok(applications); } + + @AuthorizationRequired + @GetMapping("/get-recommendations") + public ResponseEntity> getRecommendedGroups(@RequestHeader String Authorization, HttpServletRequest request) { + User user = (User) request.getAttribute("authenticatedUser"); + List groups = groupService.getRecommendedGroups(user); + return ResponseEntity.ok(groups); + } } 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/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/response/home/HomePagePostResponseDto.java b/app/backend/src/main/java/com/app/gamereview/dto/response/home/HomePagePostResponseDto.java index b08182ff..73413536 100644 --- 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 @@ -1,11 +1,13 @@ package com.app.gamereview.dto.response.home; import com.app.gamereview.enums.ForumType; +import com.app.gamereview.model.Tag; import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; @Getter @@ -35,7 +37,7 @@ public class HomePagePostResponseDto { private LocalDateTime lastEditedAt; - private List tags; + private List tags = new ArrayList<>(); private Boolean inappropriate; 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/service/GameService.java b/app/backend/src/main/java/com/app/gamereview/service/GameService.java index 9116defb..26ca90eb 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 @@ -9,11 +9,10 @@ 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.Game; -import com.app.gamereview.model.Tag; +import com.app.gamereview.model.*; import com.app.gamereview.repository.ForumRepository; import com.app.gamereview.repository.GameRepository; +import com.app.gamereview.repository.ProfileRepository; import com.app.gamereview.repository.TagRepository; import org.modelmapper.ModelMapper; import org.modelmapper.PropertyMap; @@ -23,9 +22,7 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; @Service @@ -36,18 +33,21 @@ public class GameService { 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) { + MongoTemplate mongoTemplate, TagRepository tagRepository, ForumRepository forumRepository, + ProfileRepository profileRepository, ModelMapper modelMapper) { this.gameRepository = gameRepository; this.tagRepository = tagRepository; this.mongoTemplate = mongoTemplate; this.forumRepository = forumRepository; + this.profileRepository = profileRepository; this.modelMapper = modelMapper; modelMapper.addMappings(new PropertyMap() { @@ -356,5 +356,100 @@ public Boolean deleteGame(String id){ return true; } + public List getRecommendedGames(User user){ + 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(); + + TreeSet recommendedGames = new TreeSet<>(Comparator.reverseOrder()); + + for(String gameId : followedGameIds){ + recommendedGames.addAll(recommendationByGameId(gameId)); + } + + List recommendations = new ArrayList<>(); + + for(RecommendGameDto gameDto : recommendedGames){ + recommendations.add(gameDto.getGame()); + } + + 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.GENRE)){ + score++; + } + } + } + return score; + } } + + 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 ec4ec2a2..2ebfedff 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,5 +1,6 @@ package com.app.gamereview.service; +import com.app.gamereview.dto.request.game.RecommendGameDto; import com.app.gamereview.dto.request.group.*; import com.app.gamereview.dto.response.group.GetGroupDetailResponseDto; import com.app.gamereview.dto.response.group.GetGroupResponseDto; @@ -42,6 +43,7 @@ public class GroupService { private final MongoTemplate mongoTemplate; private final ModelMapper modelMapper; + private final GameService gameService; @Autowired public GroupService( @@ -53,7 +55,8 @@ public GroupService( ProfileRepository profileRepository, GroupApplicationRepository groupApplicationRepository, MongoTemplate mongoTemplate, - ModelMapper modelMapper + ModelMapper modelMapper, + GameService gameService ) { this.groupRepository = groupRepository; this.gameRepository = gameRepository; @@ -64,6 +67,7 @@ public GroupService( this.groupApplicationRepository = groupApplicationRepository; this.mongoTemplate = mongoTemplate; this.modelMapper = modelMapper; + this.gameService = gameService; modelMapper.addMappings(new PropertyMap() { @Override @@ -569,6 +573,110 @@ private GroupApplicationResponseDto contertToDto(GroupApplication application) { dto.setStatus(application.getStatus()); return dto; } + public List getRecommendedGroups(User user){ + Optional findProfile = profileRepository.findByUserIdAndIsDeletedFalse(user.getId()); + + if(findProfile.isEmpty()){ + throw new ResourceNotFoundException("Profile of the user is not found, unexpected error has occurred"); + } + + List memberGroups = groupRepository.findUserGroups(user.getId()); + if(memberGroups.isEmpty()){ + return Collections.emptyList(); + } + TreeSet recommendedGroups = new TreeSet<>(Comparator.reverseOrder()); + for(Group group : memberGroups){ + String groupId = group.getId(); + recommendedGroups.addAll(recommendationByGroupId(groupId)); + } + + List recommendations = new ArrayList<>(); + + for(RecommendGroupDto groupDto : recommendedGroups){ + recommendations.add(groupDto.getGroup()); + } + + return recommendations; + } + + public TreeSet recommendationByGroupId(String groupId){ + 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())); + 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(); // all games except the base game + allGroupsQuery.addCriteria(Criteria.where("isDeleted").is(false)); + allGroupsQuery.addCriteria(Criteria.where("_id").nin(idList)); + + 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/PostService.java b/app/backend/src/main/java/com/app/gamereview/service/PostService.java index 94f1e06f..351e890c 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 @@ -3,6 +3,8 @@ import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; + +import com.app.gamereview.dto.request.group.CreateGroupRequestDto; import com.app.gamereview.dto.request.notification.CreateNotificationRequestDto; import com.app.gamereview.dto.request.home.HomePagePostsFilterRequestDto; import com.app.gamereview.dto.response.comment.CommentReplyResponseDto; @@ -15,6 +17,7 @@ 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; @@ -79,6 +82,13 @@ public PostService(PostRepository postRepository, ForumRepository forumRepositor 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) { @@ -623,6 +633,7 @@ else if(filter.getSortBy().equals(SortType.VOTE_COUNT.name())){ for(Post post : first20){ HomePagePostResponseDto dto = modelMapper.map(post,HomePagePostResponseDto.class); + dto.setTags(populatedTags(post.getTags())); Optional findForum = forumRepository.findByIdAndIsDeletedFalse(post.getForum()); @@ -664,4 +675,15 @@ else if(forumOfPost.getType().equals(ForumType.GAME)){ return first20dto; } + + 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/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/package.json b/app/frontend/package.json index 4283c1b6..b64be722 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -17,6 +17,7 @@ "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", diff --git a/app/frontend/pnpm-lock.yaml b/app/frontend/pnpm-lock.yaml index 4dda7ba8..8d08d618 100644 --- a/app/frontend/pnpm-lock.yaml +++ b/app/frontend/pnpm-lock.yaml @@ -26,6 +26,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 @@ -672,6 +675,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'} @@ -817,6 +930,10 @@ packages: 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: @@ -854,6 +971,18 @@ 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/prop-types@15.7.8: resolution: {integrity: sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==} dev: true @@ -1156,6 +1285,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 @@ -1205,6 +1338,10 @@ packages: /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'} @@ -1214,6 +1351,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: @@ -1250,6 +1391,26 @@ 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'} @@ -1403,6 +1564,15 @@ 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 + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1434,6 +1604,10 @@ packages: esutils: 2.0.3 dev: true + /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 @@ -1591,6 +1765,10 @@ 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 @@ -1629,6 +1807,15 @@ 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'} @@ -1694,6 +1881,22 @@ packages: 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 + + /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'} @@ -1727,6 +1930,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'} @@ -1751,6 +1961,12 @@ 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 @@ -1765,6 +1981,22 @@ packages: 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 + /hasown@2.0.0: resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} engines: {node: '>= 0.4'} @@ -1772,11 +2004,21 @@ packages: function-bind: 1.1.2 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==} @@ -1818,6 +2060,10 @@ packages: 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'} @@ -1842,6 +2088,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'} @@ -1914,6 +2164,19 @@ packages: 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'} @@ -1990,11 +2253,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'} @@ -2031,6 +2317,18 @@ packages: 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'} @@ -2045,10 +2343,18 @@ packages: engines: {node: '>= 6'} dev: false + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + 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: @@ -2080,6 +2386,10 @@ 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'} @@ -2087,6 +2397,25 @@ packages: 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 + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2110,6 +2439,15 @@ packages: 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==} @@ -2127,6 +2465,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: @@ -2212,6 +2562,11 @@ packages: 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: @@ -2224,6 +2579,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'} @@ -2237,6 +2596,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==} @@ -2913,12 +3279,32 @@ 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==} @@ -2967,6 +3353,10 @@ packages: dependencies: queue-microtask: 1.2.3 + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + /sass@1.69.4: resolution: {integrity: sha512-+qEreVhqAy8o++aQfCJwp0sklr2xyEzkm9Pp/Igu9wNPoe7EZEQ8X/MBvvXggI2ql607cxKg/RKOwDj6pp2XDA==} engines: {node: '>=14.0.0'} @@ -2976,6 +3366,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: @@ -3000,6 +3394,16 @@ 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 + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3012,6 +3416,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'} @@ -3025,6 +3437,12 @@ packages: resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} 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'} @@ -3037,6 +3455,14 @@ 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.3.0: resolution: {integrity: sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==} dev: false @@ -3128,6 +3554,10 @@ packages: engines: {node: '>=12.22'} dev: false + /timm@1.7.1: + resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==} + dev: false + /tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} dev: false @@ -3147,6 +3577,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'} @@ -3213,6 +3651,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'} @@ -3224,6 +3669,12 @@ 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 @@ -3280,6 +3731,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 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..5c39fda7 --- /dev/null +++ b/app/frontend/src/Components/Character/Character.tsx @@ -0,0 +1,39 @@ +import styles from "./Character.module.scss"; + +function Character({ + onClick, + name, + imgUrl, +}: { + onClick: () => void; + name: string; + imgUrl: 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..ca48d0c4 --- /dev/null +++ b/app/frontend/src/Components/Character/CharacterDetails.tsx @@ -0,0 +1,159 @@ +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 }: { character: any }) { + 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); + console.log(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/Forum/ForumPost/ForumPost.module.scss b/app/frontend/src/Components/Forum/ForumPost/ForumPost.module.scss index 21f2698d..d67a5dbe 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; @@ -87,6 +90,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 fd8e9835..fc524b52 100644 --- a/app/frontend/src/Components/Forum/ForumPost/ForumPost.tsx +++ b/app/frontend/src/Components/Forum/ForumPost/ForumPost.tsx @@ -26,11 +26,17 @@ function ForumPost({ 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(); @@ -57,8 +63,11 @@ function ForumPost({ ["post", post.id], ], }); + const typeStyle = + type === "GAME" ? styles.game : type === "GROUP" ? styles.group : undefined; + return ( -
+
+ {type !== "STANDARD" && ( + + )} +
+
+ ); +} + +export default CreateCharacter; diff --git a/app/frontend/src/Pages/Admin/Game/CreateGame/CreateGame.tsx b/app/frontend/src/Pages/Admin/Game/CreateGame/CreateGame.tsx index f5480e16..deab47b1 100644 --- a/app/frontend/src/Pages/Admin/Game/CreateGame/CreateGame.tsx +++ b/app/frontend/src/Pages/Admin/Game/CreateGame/CreateGame.tsx @@ -63,7 +63,7 @@ function CreateGame() { const uploadImageMutation = useMutation( (i: any) => uploadImage(i, "game-icons"), { - onError: () => { + onError: (error) => { handleAxiosError(error); }, } diff --git a/app/frontend/src/Pages/Admin/Game/DeleteGame/DeleteGame.module.scss b/app/frontend/src/Pages/Admin/Game/DeleteGame/DeleteGame.module.scss new file mode 100644 index 00000000..67ad2072 --- /dev/null +++ b/app/frontend/src/Pages/Admin/Game/DeleteGame/DeleteGame.module.scss @@ -0,0 +1,21 @@ +@import "../../../../colors"; + +.container { + padding: 4rem; + + .header { + color: red; + } + + .form { + width: 40rem; + } + + .select { + width: 50%; + } + + .button { + width: 25%; + } +} diff --git a/app/frontend/src/Pages/Admin/Game/DeleteGame/DeleteGame.tsx b/app/frontend/src/Pages/Admin/Game/DeleteGame/DeleteGame.tsx new file mode 100644 index 00000000..ed92a6ae --- /dev/null +++ b/app/frontend/src/Pages/Admin/Game/DeleteGame/DeleteGame.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import { useMutation, useQuery } from "react-query"; +import styles from "./DeleteGame.module.scss"; +import "react-datepicker/dist/react-datepicker.css"; +import { Button } from "antd"; +import { deleteGame, getGames } from "../../../../Services/games"; +import SingleSelect from "../../../../Components/SingleSelect/SingleSelect"; +import { handleError } from "../../../../Library/utils/handleError"; +import { NotificationUtil } from "../../../../Library/utils/notification"; + +function DeleteGame() { + const [gameName, setGameName] = useState(""); + + const deleteGameMutation = useMutation(deleteGame, { + onSuccess: async () => { + NotificationUtil.success("You successfully delete the game."); + }, + onError: (error) => { + handleError(error); + }, + }); + + const { data: games } = useQuery(["games"], () => getGames()); + + const handleClick = async () => { + const game = games.find((game) => game.gameName === gameName); + + deleteGameMutation.mutate(game.id); + }; + + const onChange = (_filterKey: string, value: string) => { + setGameName(value); + }; + return ( +
+

Delete Game

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

+ +
+
+ ); +} + +export default DeleteGame; diff --git a/app/frontend/src/Pages/Admin/Game/UpdateGame/UpdateGame.module.scss b/app/frontend/src/Pages/Admin/Game/UpdateGame/UpdateGame.module.scss new file mode 100644 index 00000000..d78ac5a9 --- /dev/null +++ b/app/frontend/src/Pages/Admin/Game/UpdateGame/UpdateGame.module.scss @@ -0,0 +1,38 @@ +@import "../../../../colors"; + +.container { + padding: 4rem; + + .header { + color: green; + } + + .form { + width: 40rem; + } + + .select { + width: 100%; + } + + .input { + margin-bottom: 2rem; + } + .colorHeader { + color: #6ea2bb; + margin: 2rem 0 0.5rem 0; + } + + .datePicker { + display: block; + margin-bottom: 2rem; + } + + .button { + width: 25%; + } + + .picker { + margin-bottom: 2rem; + } +} diff --git a/app/frontend/src/Pages/Admin/Game/UpdateGame/UpdateGame.tsx b/app/frontend/src/Pages/Admin/Game/UpdateGame/UpdateGame.tsx new file mode 100644 index 00000000..2cb8b7d7 --- /dev/null +++ b/app/frontend/src/Pages/Admin/Game/UpdateGame/UpdateGame.tsx @@ -0,0 +1,161 @@ +import { useState } from "react"; +import { useMutation, useQuery } from "react-query"; +import DatePicker from "react-datepicker"; +import styles from "./UpdateGame.module.scss"; +import "react-datepicker/dist/react-datepicker.css"; +import { Button, Input, Upload } from "antd"; +import TextArea from "antd/es/input/TextArea"; +import { InboxOutlined } from "@ant-design/icons"; +import { uploadImage } from "../../../../Services/image"; +import { NotificationUtil } from "../../../../Library/utils/notification"; +import { handleAxiosError } from "../../../../Library/utils/handleError"; +import { getGames, updateGame } from "../../../../Services/games"; +import SingleSelect from "../../../../Components/SingleSelect/SingleSelect"; +import { getGame } from "../../../../Services/gamedetail"; + +function UpdateGame() { + const [id, setId] = useState(""); + const [game, setGame] = useState(null); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [releaseDate, setReleaseDate] = useState(new Date()); + const [fileList, setFileList] = useState([]); + + const { data: games } = useQuery(["games"], () => getGames()); + + const { Dragger } = Upload; + const updateGameMutation = useMutation(updateGame, { + onSuccess: async () => { + NotificationUtil.success("You successfully update the game."); + }, + onError: (error) => { + console.log(error); + handleAxiosError(error); + }, + }); + + const uploadImageMutation = useMutation( + (i: any) => uploadImage(i, "game-icons"), + { + onError: (error) => { + handleAxiosError(error); + }, + } + ); + const handleClick = async () => { + let gameIcon; + if (fileList.length > 0) { + console.log(fileList); + gameIcon = await uploadImageMutation.mutateAsync( + fileList[0].originFileObj + ); + } + + updateGameMutation.mutate({ + id, + name, + description, + releaseDate, + gameIcon: gameIcon || game?.gameIcon, + minSystemReq: game.minSystemReq, + }); + }; + + 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) => { + let updatedGame = games.find((game) => game.gameName === value); + updatedGame = await getGame(updatedGame.id); + setId(updatedGame.id); + setGame(updatedGame); + console.log(updatedGame); + setName(updatedGame.gameName); + setDescription(updatedGame.gameDescription); + + const dateObject = new Date(Date.parse(updatedGame.releaseDate)); + + setReleaseDate(dateObject); + }; + + return ( +
+

Update Game

+ {games?.length > 0 && ( + game.gameName)} + onChange={onGameChange} + reset={false} + > + )} + {game && ( +
+ setName(event.target.value)} + /> +