diff --git a/build.gradle b/build.gradle index 2bc40119073eb28d5de1c42825e41e177a753f49..ace6ecf23ddb939e56918a48ca5e28f7aed824c6 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ repositories { } dependencies { + implementation 'com.google.code.gson:gson:2.8.9' implementation 'commons-io:commons-io:2.8.0' implementation 'io.jsonwebtoken:jjwt:0.9.1' implementation 'org.springframework.session:spring-session-jdbc:2.4.3' diff --git a/src/main/java/com/example/clientproject/web/restControllers/ShopSearch.java b/src/main/java/com/example/clientproject/web/restControllers/ShopSearch.java new file mode 100644 index 0000000000000000000000000000000000000000..10e2013334d34db0133cdad585ef95f87514be53 --- /dev/null +++ b/src/main/java/com/example/clientproject/web/restControllers/ShopSearch.java @@ -0,0 +1,139 @@ +package com.example.clientproject.web.restControllers; + +import com.example.clientproject.data.shops.Shops; +import com.example.clientproject.data.shops.ShopsRepo; +import com.example.clientproject.data.tags.Tags; +import com.example.clientproject.data.tags.TagsRepo; +import com.example.clientproject.services.RecommendationGenerator; +import com.google.gson.Gson; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpSession; +import java.util.*; +import java.util.stream.Collectors; + +@RestController +public class ShopSearch { + @Autowired + ShopsRepo shopsRepo; + + @Autowired + TagsRepo tagsRepo; + + @Autowired + RecommendationGenerator recommendationGenerator; + + @GetMapping("/shop/search") + public String searchShops(@RequestParam(value = "q", required = false) String query, + @RequestParam(value = "p", required = false) Integer page, + @RequestParam(value = "t", required = false) List<String> tags, + HttpSession session) throws Exception { + final Integer ITEMS_PER_PAGE = 6; + + //Get all the active shops + List<Shops> allShops = shopsRepo.findActiveShops(); + + //Filter the shops using the query provided + if(query != null){ + allShops = allShops + .stream() + .filter(s -> s.getShopName().toLowerCase(Locale.ROOT).strip().contains(query.toLowerCase(Locale.ROOT).strip())) + .collect(Collectors.toList()); + } + + //Filter using the tags provided + if(tags!=null){ + List<Long> validTagIds = new ArrayList<>(); + for (String t : tags){ + Optional<Tags> tagsOptional = tagsRepo.findByTagNameIgnoreCase(t); + if(tagsOptional.isPresent()){ + Long tagId = tagsOptional.get().getTagId(); + if (!validTagIds.contains(tagId)){ + validTagIds.add(tagId); + } + } + } + List<Shops> validShops = new ArrayList<>(); + for (Shops s : allShops){ + boolean match = false; + for (Tags t : s.getShopTags()){ + if (validTagIds.contains(t.getTagId())){ + match = true; + break; + } + } + if (match){ + validShops.add(s); + } + } + allShops = validShops; + } + + //Paginate + boolean hasNextPage = false; + if (allShops.size() > ITEMS_PER_PAGE){ + if(page==null){ + page = 1; + } + List<List<Shops>> pages = getPages(allShops, ITEMS_PER_PAGE); + if(page > pages.size()){ + page = 1; + } + if (pages.size() >= page){ + allShops = pages.get(page-1); + } + if (pages.size() >= page + 1){ + hasNextPage = true; + } + } + + //Sort in order of relevance + allShops = recommendationGenerator.getRecommendations(session, allShops); + + //Convert to required format + List<HashMap<String, String>> formattedShops = new ArrayList<>(); + for(Shops shop : allShops){ + HashMap<String,String> data = new HashMap<>(); + data.put("name",shop.getShopName()); + data.put("banner",shop.getShopBanner()); + data.put("id", String.valueOf(shop.getShopId())); + data.put("category",shop.getCategory().getCategoryName()); + data.put("website",shop.getShopWebsite()); + Integer reward_count = shop.getStampBoard().getRewards().size(); + data.put("reward_count",String.valueOf(reward_count)); + if(reward_count != 0){ + data.put("next_reward_name",shop.getStampBoard().getRewards().get(0).getRewardName()); + data.put("next_reward_pos",String.valueOf(shop.getStampBoard().getRewards().get(0).getRewardStampLocation())); + }else{ + data.put("next_reward_name","No Rewards"); + } + formattedShops.add(data); + } + + Map<String,Object> returnMap = new HashMap<>(); + returnMap.put("shops",formattedShops); + returnMap.put("hasNextPage", hasNextPage); + + Gson gson = new Gson(); + String json = gson.toJson(returnMap); + + return json; + + } + + public <T> List<List<T>> getPages(Collection<T> c, Integer pageSize) { + if (c == null) + return Collections.emptyList(); + List<T> list = new ArrayList<T>(c); + if (pageSize == null || pageSize <= 0 || pageSize > list.size()) + pageSize = list.size(); + int numPages = (int) Math.ceil((double)list.size() / (double)pageSize); + List<List<T>> pages = new ArrayList<List<T>>(numPages); + for (int pageNum = 0; pageNum < numPages;) + pages.add(list.subList(pageNum * pageSize, Math.min(++pageNum * pageSize, list.size()))); + return pages; + } +} diff --git a/src/main/resources/static/imgs/uploaded/02b3324f_113c_4c98_8ad6_7f1cf28f74c9.jpg b/src/main/resources/static/imgs/uploaded/02b3324f_113c_4c98_8ad6_7f1cf28f74c9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7ff5ef91293d6692910338be0f6b54dd16674c6a Binary files /dev/null and b/src/main/resources/static/imgs/uploaded/02b3324f_113c_4c98_8ad6_7f1cf28f74c9.jpg differ diff --git a/src/main/resources/static/imgs/uploaded/1eab1fb2_7744_4eb6_9505_8a1e9b59981b.jpg b/src/main/resources/static/imgs/uploaded/1eab1fb2_7744_4eb6_9505_8a1e9b59981b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7ff5ef91293d6692910338be0f6b54dd16674c6a Binary files /dev/null and b/src/main/resources/static/imgs/uploaded/1eab1fb2_7744_4eb6_9505_8a1e9b59981b.jpg differ diff --git a/src/main/resources/static/imgs/uploaded/7615374a_c54e_4854_ad96_14b719c5af9c.jpg b/src/main/resources/static/imgs/uploaded/7615374a_c54e_4854_ad96_14b719c5af9c.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7ff5ef91293d6692910338be0f6b54dd16674c6a Binary files /dev/null and b/src/main/resources/static/imgs/uploaded/7615374a_c54e_4854_ad96_14b719c5af9c.jpg differ diff --git a/src/main/resources/static/imgs/uploaded/8d96f724_c612_4da0_9ab0_26a0ea2ad161.jpg b/src/main/resources/static/imgs/uploaded/8d96f724_c612_4da0_9ab0_26a0ea2ad161.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7ff5ef91293d6692910338be0f6b54dd16674c6a Binary files /dev/null and b/src/main/resources/static/imgs/uploaded/8d96f724_c612_4da0_9ab0_26a0ea2ad161.jpg differ diff --git a/src/main/resources/static/js/searchBar.js b/src/main/resources/static/js/searchBar.js new file mode 100644 index 0000000000000000000000000000000000000000..ebce210a4fbe540bbc37b9e7735226c86aede06e --- /dev/null +++ b/src/main/resources/static/js/searchBar.js @@ -0,0 +1,115 @@ +var tags = []; +var query = ""; +var page = 1; + +var searchBar = document.getElementById("main-search"); +var tagGroup = document.getElementById("search-tag-group"); + +function updateSearch(e){ + page = 1; + query = searchBar.value; + doSearch(false) +} + +function removeTag(i){ + tags.splice(i, 1); + page=1; + updateUI() + doSearch(false) +} + +function addTag(e){ + if(e.key== "Enter"){ + if (searchBar.value != ""){ + page = 1; + query = ""; + tags.push(searchBar.value.toLowerCase()); + searchBar.value = ""; + updateUI(); + doSearch(false); + } + } +} + +function updateUI(){ + tagGroup.innerHTML = ""; + for(let [i,tag] of tags.entries()){ + tagGroup.innerHTML += ` + <div class="control mr-3"> + <div class="tags has-addons"> + <span class="tag gradient">${tag}</span> + <a class="tag is-delete" onclick="removeTag(${i})"></a> + </div> + </div>` + } +} + +function doSearch(fromNextPageBtn){ + let url = "/shop/search" + url += "?q=" + query + url += "&p=" + page.toString() + for (let t of tags){ + url += "&t=" + t.toString() + } + fetch(url) + .then(e=>e.json()) + .then(data=>{ + if(!fromNextPageBtn){ + document.getElementById("business_card_container").innerHTML = ""; + } + for(let shop of data["shops"]){ + addShop(shop); + } + if(data["hasNextPage"] == true){ + document.getElementById("loadMoreBtn").style.display = "flex"; + }else{ + document.getElementById("loadMoreBtn").setAttribute('style', 'display:none!important'); + } + }); +} + +function addShop(shopInfo){ + let img_path = shopInfo["banner"] + let title = shopInfo["name"] + let reward_text = shopInfo["next_reward_name"] + " at " + shopInfo["next_reward_pos"] + " stamps" + let reward_amount = shopInfo["reward_count"] + let shopId = shopInfo["id"] + + document.getElementById("business_card_container").innerHTML +=` +<div class="business_container box" style="position:relative;"> + <div class="image" style="background-image:url(${img_path});"></div> + <div class="favouriteStar starContainer" onclick="favouriteBusiness(this,${shopId});"> + <span class="icon favouriteStar"> + <i class="far fa-star"></i> + </span> + <span class="icon favouriteStar"> + <i class="fas fa-star"></i> + </span> + </div> + <div class="content"> + <h1 class="title is-4 mb-1">${title}</h1> + <p class="mb-1">${reward_text}</p> + <div class="is-full-width" style="display:flex;justify-content: space-between;align-items: center"> + <div class="level-left"> + <span class="icon is-small is-left ml-1 mr-1"> + <i class="fas fa-gift"></i> + </span> + <p>${reward_amount}</p> + </div> + <div class="level-right"> + <button class="button is-rounded gradient" onclick="redirect(${shopId})"> + View Shop + <span class="icon is-small is-left ml-1"> + <i class="fas fa-arrow-right"></i> + </span> + </button> + </div> + </div> + </div> +</div>` +} + +function loadNextPage(){ + page++; + doSearch(true); +} \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 7d36dcf8a9fd8fda6c08e8e55ffa21909878a404..041a8e83fd9889a493a376d54c50066a4192ba85 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -62,22 +62,24 @@ <div class="container is-full-width is-flex is-justify-content-center is-align-items-center is-flex-direction-column mb-4"> <h1 class="title is-3">Where else can I earn rewards?</h1> <div class="control has-icons-left mb-2" style="width: 60%;"> - <input class="input" type="text" placeholder="Enter Brands or keywords e.g. Vegan, Clothing etc.."> + <input class="input" type="text" placeholder="Enter Brands or keywords e.g. Vegan, Clothing etc.." + oninput="updateSearch(event)" onkeydown="addTag(event)" id="main-search"> <span class="icon is-small is-left"> <i class="fas fa-search"></i> </span> </div> <!--Tags--> - <div class="field is-grouped is-grouped-multiline" style="width: 60%;"> - <div th:each="tag: ${tags}" th:include="fragments/tag.html :: tag" - th:with="text=${tag}"></div> + <div class="field is-grouped is-grouped-multiline" style="width: 60%;" id="search-tag-group"> +<!-- <div th:each="tag: ${tags}" th:include="fragments/tag.html :: tag"--> +<!-- th:with="text=${tag}"></div>--> </div> </div> - <div class="container is-full-width is-flex is-justify-content-center is-align-items-center is-flex-wrap-wrap"> - <div th:each="shop,i: ${normalShops}" th:include="fragments/business_card.html :: business_card" - th:with="title=${shop.shopName}, reward_text='Free coffee at 6 stamps', reward_amount=4, - img_path=${shop.shopImage}, shopId=${shop.shopId}"></div> + <div class="container is-full-width is-flex is-justify-content-center is-align-items-center is-flex-wrap-wrap" + id="business_card_container"> + </div> + <div class="is-full-width is-flex is-justify-content-center is-align-items-center" id="loadMoreBtn" style="display: none!important;;"> + <a onclick="loadNextPage()">Load More</a> </div> </div> @@ -97,5 +99,7 @@ </div> </div> </div> + <script src="/js/searchBar.js"></script> + <script>doSearch(false);</script> </body> </html>