-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#41 - Added minimal web UI for Starbucks example app.
Added a minimalistic HTML5 web front-end based on Thymeleaf, Bootstrap, jQuery, URI.js and Google Maps JavaScript API. The required JavaScript dependencies are referenced via Webjars. For details see the README. Original pull request: #47.
- Loading branch information
Showing
8 changed files
with
343 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
rest/starbucks/src/main/java/example/stores/web/StoresController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/* | ||
* Copyright 2014-2015 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package example.stores.web; | ||
|
||
import static org.springframework.data.geo.Metrics.*; | ||
|
||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
|
||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.data.domain.Page; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.data.geo.Distance; | ||
import org.springframework.data.geo.Metrics; | ||
import org.springframework.data.geo.Point; | ||
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; | ||
import org.springframework.stereotype.Controller; | ||
import org.springframework.ui.Model; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RequestMethod; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
|
||
import example.stores.Store; | ||
import example.stores.StoreRepository; | ||
|
||
/** | ||
* A Spring MVC controller to produce an HTML frontend. | ||
* | ||
* @author Oliver Gierke | ||
*/ | ||
@Controller | ||
@RequiredArgsConstructor(onConstructor = @__(@Autowired)) | ||
class StoresController { | ||
|
||
private static final List<Distance> DISTANCES = Arrays.asList(new Distance(0.5, MILES), new Distance(1, MILES), | ||
new Distance(2, MILES)); | ||
private static final Distance DEFAULT_DISTANCE = new Distance(1, Metrics.MILES); | ||
private static final Map<String, Point> KNOWN_LOCATIONS; | ||
|
||
static { | ||
|
||
Map<String, Point> locations = new HashMap<>(); | ||
|
||
locations.put("Pivotal SF", new Point(-122.4041764, 37.7819286)); | ||
locations.put("Timesquare NY", new Point(-73.995146, 40.740337)); | ||
|
||
KNOWN_LOCATIONS = Collections.unmodifiableMap(locations); | ||
} | ||
|
||
private final StoreRepository repository; | ||
private final RepositoryEntityLinks entityLinks; | ||
|
||
/** | ||
* Looks up the stores in the given distance around the given location. | ||
* | ||
* @param model the {@link Model} to populate. | ||
* @param location the optional location, if none is given, no search results will be returned. | ||
* @param distance the distance to use, if none is given the {@link #DEFAULT_DISTANCE} is used. | ||
* @param pageable the pagination information | ||
* @return | ||
*/ | ||
@RequestMapping(value = "/", method = RequestMethod.GET) | ||
String index(Model model, @RequestParam Optional<Point> location, @RequestParam Optional<Distance> distance, | ||
Pageable pageable) { | ||
|
||
Point point = location.orElse(KNOWN_LOCATIONS.get("Timesquare NY")); | ||
|
||
Page<Store> stores = repository.findByAddressLocationNear(point, distance.orElse(DEFAULT_DISTANCE), pageable); | ||
|
||
model.addAttribute("stores", stores); | ||
model.addAttribute("distances", DISTANCES); | ||
model.addAttribute("selectedDistance", distance.orElse(DEFAULT_DISTANCE)); | ||
model.addAttribute("location", point); | ||
model.addAttribute("locations", KNOWN_LOCATIONS); | ||
model.addAttribute("api", entityLinks.linkToSearchResource(Store.class, "by-location", pageable).getHref()); | ||
|
||
return "index"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
spring.data.rest.base-path=/api |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
<!DOCTYPE html> | ||
<html lang="en" xmlns:th="http://www.thymeleaf.org" > | ||
<head> | ||
<title>Starbucks Storefinder</title> | ||
|
||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
|
||
<link rel="stylesheet" href="/webjars/bootstrap/3.3.4/css/bootstrap.min.css"/> | ||
|
||
<style type="text/css"> | ||
|
||
#map { | ||
height: 400px; | ||
width: 400px; | ||
background-image: url("/img/map.png"); | ||
background-size: 400px 300px; | ||
background-repeat: no-repeat; | ||
} | ||
|
||
</style> | ||
</head> | ||
|
||
<body> | ||
|
||
<div class="container-fluid"> | ||
|
||
<h1>Starbucks Storefinder</h1> | ||
|
||
<div class="panel panel-default"> | ||
<div class="panel-heading"> | ||
<h3 class="panel-title">Search</h3> | ||
</div> | ||
<div class="panel-body"> | ||
|
||
<form action="/" class="form-horizontal"> | ||
|
||
<div class="form-group"> | ||
<label class="col-sm-2 control-label">Predefined locations:</label> | ||
<div class="col-sm-10"> | ||
<a class="btn btn-default" th:each="location : ${locations}" th:href="@{/(location=${{location.value}},distance=${{selectedDistance}})}" th:text="${location.key}">Foo</a> | ||
</div> | ||
</div> | ||
|
||
<div class="form-group"> | ||
<label for="location" class="col-sm-2 control-label">Location:</label> | ||
<div class="col-sm-2"> | ||
<input id="location" name="location" th:value="${{location}}" type="text" class="form-control" placeholder="lat,long" /> | ||
</div> | ||
</div> | ||
|
||
|
||
<div class="form-group"> | ||
<label for="distance" class="col-sm-2 control-label">Distance:</label> | ||
<div class="col-sm-2"> | ||
<select id="distance" name="distance" class="form-control"> | ||
<option th:each="distance : ${distances}" th:value="${{distance}}" th:text="${distance}" th:selected="${distance == selectedDistance}"> | ||
Distance | ||
</option> | ||
</select> | ||
</div> | ||
</div> | ||
|
||
<div class="form-group"> | ||
<div class="col-sm-offset-2 col-sm-10"> | ||
<button id="submit" type="submit" class="btn btn-default">Submit</button> | ||
</div> | ||
</div> | ||
|
||
</form> | ||
</div> | ||
</div> | ||
|
||
<div class="panel panel-default"> | ||
|
||
<div id="resultList" class="panel-heading"> | ||
<h3 class="panel-title">Results</h3> | ||
</div> | ||
|
||
<div class="panel-body"> | ||
|
||
<div id="map" class="col-md-4" th:attr="data-uri=${api}"></div> | ||
|
||
<div class="col-sm-8" style="margin-left: 1em"> | ||
<div th:if="${stores.hasContent()}"> | ||
<p th:text="'Showing ' + ${stores.numberOfElements} + ' of ' + ${stores.totalElements} + ' results.'">Found 1 results.</p> | ||
<ol class="search-results"> | ||
<li class="search-result" th:each="store : ${stores}" th:text="${store.name} + ' - ' + ${store.address}">Store name</li> | ||
</ol> | ||
</div> | ||
<p class="search-result no-results" th:unless="${stores.hasContent()}">No Results</p> | ||
</div> | ||
|
||
</div> | ||
|
||
</div> | ||
</div> | ||
|
||
<script type="text/javascript" | ||
src="/webjars/jquery/2.1.3/jquery.min.js"></script> | ||
|
||
<script type="text/javascript" | ||
src="/webjars/bootstrap/3.3.4/js/bootstrap.min.js"></script> | ||
|
||
<script type="text/javascript" | ||
src="/webjars/URI.js/1.14.1/URI.min.js"></script> | ||
|
||
<script type="text/javascript" | ||
src="//maps.google.com/maps/api/js?sensor=false"></script> | ||
|
||
<script type="text/javascript"> | ||
(function () { | ||
"use strict"; | ||
|
||
function initApp() { | ||
|
||
function handleSearchResult(searchResponse) { | ||
|
||
|
||
} | ||
|
||
window.starbucks = { | ||
|
||
ui: { | ||
markers: [], | ||
map: null | ||
}, | ||
|
||
performStoreSearch: function () { | ||
|
||
// Maps enabled (online)? | ||
|
||
if (!starbucks.ui.map) { | ||
return; | ||
} | ||
|
||
// Get location | ||
|
||
var location = $("#location").val(); | ||
|
||
// Center map | ||
|
||
var coordinate = { | ||
lat: parseFloat(location.split(",")[0]), | ||
lng: parseFloat(location.split(",")[1]) | ||
} | ||
|
||
starbucks.ui.map.setCenter(coordinate); | ||
|
||
new google.maps.Marker({ | ||
position: coordinate, | ||
map: starbucks.ui.map | ||
}); | ||
|
||
// Expand template and execute search | ||
|
||
var template = new URITemplate($("#map").attr("data-uri")); | ||
|
||
$.get(template.expand({ | ||
"location": location, | ||
"distance": $("#distance").val(), | ||
"size": 100, | ||
"page": 0 | ||
}), function(response) { | ||
|
||
while (starbucks.ui.markers.length) { | ||
starbucks.ui.markers.pop().setMap(null); | ||
} | ||
|
||
// Create marker for store | ||
|
||
response._embedded["stores"].forEach(function (store) { | ||
starbucks.ui.markers.push(new google.maps.Marker({ | ||
position: { | ||
lat: store.address.location.y, | ||
lng: store.address.location.x | ||
}, | ||
map: starbucks.ui.map | ||
})); | ||
}); | ||
}) | ||
}, | ||
|
||
init: function() { | ||
starbucks.ui.map = new google.maps.Map($("#map")[0], { zoom: 14 }); | ||
starbucks.performStoreSearch(); | ||
} | ||
}; | ||
|
||
window.starbucks.init(); | ||
} | ||
|
||
$(initApp); | ||
})(); | ||
</script> | ||
</body> | ||
</html> |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.