From d96d5bbe6301ce85783cd3f1015f8c94b086d347 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Tue, 24 Jan 2023 12:49:33 +0100 Subject: [PATCH] Added basic search service, and improved web app UI. --- .../controller/GymController.java | 15 +- .../gymboard_api/dao/GymRepository.java | 3 +- .../gymboard_api/service/GymService.java | 24 ++ gymboard-app/src/api/gymboard-api.ts | 17 +- gymboard-app/src/api/gymboard-search.ts | 26 ++ gymboard-app/src/components/SimpleGymItem.vue | 26 ++ .../src/components/StandardCenteredPage.vue | 18 + gymboard-app/src/pages/IndexPage.vue | 83 ++++- gymboard-app/src/pages/gym/GymHomePage.vue | 15 + .../src/pages/gym/GymLeaderboardsPage.vue | 15 + gymboard-app/src/pages/{ => gym}/GymPage.vue | 23 +- .../src/pages/gym/GymSubmissionPage.vue | 33 ++ gymboard-app/src/router/routes.ts | 17 +- gymboard-search/.gitignore | 35 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 58727 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + gymboard-search/README.md | 5 + gymboard-search/mvnw | 316 ++++++++++++++++++ gymboard-search/mvnw.cmd | 188 +++++++++++ gymboard-search/pom.xml | 62 ++++ .../andrewlalis/gymboardsearch/DbUtils.java | 15 + .../gymboardsearch/GymIndexGenerator.java | 71 ++++ .../gymboardsearch/GymIndexSearcher.java | 55 +++ .../GymboardSearchApplication.java | 23 ++ .../gymboardsearch/SearchController.java | 22 ++ .../gymboardsearch/config/WebConfig.java | 26 ++ .../gymboardsearch/dto/GymResponse.java | 29 ++ .../application-development.properties | 1 + .../src/main/resources/application.properties | 1 + .../src/main/resources/sql/select-gyms.sql | 14 + .../GymboardSearchApplicationTests.java | 13 + 31 files changed, 1152 insertions(+), 41 deletions(-) create mode 100644 gymboard-app/src/api/gymboard-search.ts create mode 100644 gymboard-app/src/components/SimpleGymItem.vue create mode 100644 gymboard-app/src/components/StandardCenteredPage.vue create mode 100644 gymboard-app/src/pages/gym/GymHomePage.vue create mode 100644 gymboard-app/src/pages/gym/GymLeaderboardsPage.vue rename gymboard-app/src/pages/{ => gym}/GymPage.vue (60%) create mode 100644 gymboard-app/src/pages/gym/GymSubmissionPage.vue create mode 100644 gymboard-search/.gitignore create mode 100644 gymboard-search/.mvn/wrapper/maven-wrapper.jar create mode 100644 gymboard-search/.mvn/wrapper/maven-wrapper.properties create mode 100644 gymboard-search/README.md create mode 100755 gymboard-search/mvnw create mode 100644 gymboard-search/mvnw.cmd create mode 100644 gymboard-search/pom.xml create mode 100644 gymboard-search/src/main/java/nl/andrewlalis/gymboardsearch/DbUtils.java create mode 100644 gymboard-search/src/main/java/nl/andrewlalis/gymboardsearch/GymIndexGenerator.java create mode 100644 gymboard-search/src/main/java/nl/andrewlalis/gymboardsearch/GymIndexSearcher.java create mode 100644 gymboard-search/src/main/java/nl/andrewlalis/gymboardsearch/GymboardSearchApplication.java create mode 100644 gymboard-search/src/main/java/nl/andrewlalis/gymboardsearch/SearchController.java create mode 100644 gymboard-search/src/main/java/nl/andrewlalis/gymboardsearch/config/WebConfig.java create mode 100644 gymboard-search/src/main/java/nl/andrewlalis/gymboardsearch/dto/GymResponse.java create mode 100644 gymboard-search/src/main/resources/application-development.properties create mode 100644 gymboard-search/src/main/resources/application.properties create mode 100644 gymboard-search/src/main/resources/sql/select-gyms.sql create mode 100644 gymboard-search/src/test/java/nl/andrewlalis/gymboardsearch/GymboardSearchApplicationTests.java diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/GymController.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/GymController.java index 0e0d4d0..d30df2e 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/GymController.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/controller/GymController.java @@ -2,16 +2,14 @@ package nl.andrewlalis.gymboard_api.controller; import nl.andrewlalis.gymboard_api.controller.dto.GymResponse; import nl.andrewlalis.gymboard_api.service.GymService; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.List; /** * Controller for accessing a particular gym. */ @RestController -@RequestMapping(path = "/gyms/{countryCode}/{cityCode}/{gymName}") public class GymController { private final GymService gymService; @@ -19,7 +17,7 @@ public class GymController { this.gymService = gymService; } - @GetMapping + @GetMapping(path = "/gyms/{countryCode}/{cityCode}/{gymName}") public GymResponse getGym( @PathVariable String countryCode, @PathVariable String cityCode, @@ -27,4 +25,9 @@ public class GymController { ) { return gymService.getGym(countryCode, cityCode, gymName); } + + @GetMapping(path = "/gyms/search") + public List searchGyms(@RequestParam(name = "query", required = false) String query) { + return gymService.searchGyms(query); + } } diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/GymRepository.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/GymRepository.java index f0b0c03..961f9ce 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/GymRepository.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/dao/GymRepository.java @@ -3,13 +3,14 @@ package nl.andrewlalis.gymboard_api.dao; import nl.andrewlalis.gymboard_api.model.Gym; import nl.andrewlalis.gymboard_api.model.GymId; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository -public interface GymRepository extends JpaRepository { +public interface GymRepository extends JpaRepository, JpaSpecificationExecutor { @Query("SELECT g FROM Gym g " + "WHERE g.id.shortName = :gym AND " + "g.id.city.id.shortName = :city AND " + diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/GymService.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/GymService.java index 3aff7f7..26cdc4a 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/GymService.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/service/GymService.java @@ -1,5 +1,6 @@ package nl.andrewlalis.gymboard_api.service; +import jakarta.persistence.criteria.Predicate; import nl.andrewlalis.gymboard_api.controller.dto.GymResponse; import nl.andrewlalis.gymboard_api.dao.GymRepository; import nl.andrewlalis.gymboard_api.model.Gym; @@ -8,6 +9,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; +import java.util.ArrayList; +import java.util.List; + @Service public class GymService { private final GymRepository gymRepository; @@ -22,4 +26,24 @@ public class GymService { .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); return new GymResponse(gym); } + + @Transactional(readOnly = true) + public List searchGyms(String queryText) { + return gymRepository.findAll((root, query, criteriaBuilder) -> { + query.distinct(true); + List predicates = new ArrayList<>(); + if (queryText != null && !queryText.isBlank()) { + String queryTextStr = "%" + queryText.toUpperCase() + "%"; + predicates.add(criteriaBuilder.like( + criteriaBuilder.upper(root.get("displayName")), + queryTextStr + )); + predicates.add(criteriaBuilder.like( + criteriaBuilder.upper(root.get("id").get("shortName")), + queryTextStr + )); + } + return criteriaBuilder.or(predicates.toArray(new Predicate[0])); + }).stream().map(GymResponse::new).toList(); + } } diff --git a/gymboard-app/src/api/gymboard-api.ts b/gymboard-app/src/api/gymboard-api.ts index eda8572..64f83d6 100644 --- a/gymboard-app/src/api/gymboard-api.ts +++ b/gymboard-app/src/api/gymboard-api.ts @@ -1,10 +1,15 @@ -import axios from "axios"; -import process from "process"; +import axios from 'axios'; -const api = axios.create({ +const api = axios.create({ baseURL: 'http://localhost:8080' }); +export interface GymIdentifiable { + countryCode: string, + cityShortName: string, + shortName: string +} + export type Gym = { countryCode: string, countryName: string, @@ -37,4 +42,8 @@ export async function getGym(countryCode: string, cityShortName: string, gymShor streetAddress: d.streetAddress }; return gym; -} \ No newline at end of file +} + +export function getGymRoute(gym: GymIdentifiable): string { + return `/g/${gym.countryCode}/${gym.cityShortName}/${gym.shortName}` +} diff --git a/gymboard-app/src/api/gymboard-search.ts b/gymboard-app/src/api/gymboard-search.ts new file mode 100644 index 0000000..4879fe1 --- /dev/null +++ b/gymboard-app/src/api/gymboard-search.ts @@ -0,0 +1,26 @@ +import axios from 'axios'; + +const api = axios.create({ + baseURL: 'http://localhost:8081' +}); + +export interface GymSearchResult { + shortName: string, + displayName: string, + cityShortName: string, + cityName: string, + countryCode: string, + countryName: string, + streetAddress: string, + latitude: number, + longitude: number +} + +/** + * Searches for gyms using the given query, and eventually returns results. + * @param query The query to use. + */ +export async function searchGyms(query: string): Promise> { + const response = await api.get('/search/gyms?q=' + query); + return response.data; +} diff --git a/gymboard-app/src/components/SimpleGymItem.vue b/gymboard-app/src/components/SimpleGymItem.vue new file mode 100644 index 0000000..4139e70 --- /dev/null +++ b/gymboard-app/src/components/SimpleGymItem.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/gymboard-app/src/components/StandardCenteredPage.vue b/gymboard-app/src/components/StandardCenteredPage.vue new file mode 100644 index 0000000..7878a7c --- /dev/null +++ b/gymboard-app/src/components/StandardCenteredPage.vue @@ -0,0 +1,18 @@ + + diff --git a/gymboard-app/src/pages/IndexPage.vue b/gymboard-app/src/pages/IndexPage.vue index 9189fbe..d54e529 100644 --- a/gymboard-app/src/pages/IndexPage.vue +++ b/gymboard-app/src/pages/IndexPage.vue @@ -1,20 +1,77 @@ - diff --git a/gymboard-app/src/pages/gym/GymHomePage.vue b/gymboard-app/src/pages/gym/GymHomePage.vue new file mode 100644 index 0000000..2d1e1b8 --- /dev/null +++ b/gymboard-app/src/pages/gym/GymHomePage.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/gymboard-app/src/pages/gym/GymLeaderboardsPage.vue b/gymboard-app/src/pages/gym/GymLeaderboardsPage.vue new file mode 100644 index 0000000..404e098 --- /dev/null +++ b/gymboard-app/src/pages/gym/GymLeaderboardsPage.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/gymboard-app/src/pages/GymPage.vue b/gymboard-app/src/pages/gym/GymPage.vue similarity index 60% rename from gymboard-app/src/pages/GymPage.vue rename to gymboard-app/src/pages/gym/GymPage.vue index 4e60860..93cc1e5 100644 --- a/gymboard-app/src/pages/GymPage.vue +++ b/gymboard-app/src/pages/gym/GymPage.vue @@ -1,14 +1,13 @@