From 06d7f219bb244c1944e6a3481253c241247358ec Mon Sep 17 00:00:00 2001 From: zajonc-gugle Date: Fri, 25 Dec 2020 14:17:48 +0100 Subject: [PATCH 01/18] Add a service layer between client and backend. Introduce an additional abstraction (service.js) that takes care of communication between the JS client and the Java server. The main reason to introduce it is to allow mocking the service - this way we can run a frontend app locally, even if the Java backend is down. The *real* reason is that I haven't managed to start the Google App Engine, because I forgot my security key, so I'm hacking things to unblock myself. --- src/main/webapp/index.html | 3 +- src/main/webapp/meal-page-script.js | 16 +++--- src/main/webapp/meal.html | 3 +- src/main/webapp/search-meal-script.js | 6 ++- src/main/webapp/search-results.html | 3 +- src/main/webapp/service.js | 75 +++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 src/main/webapp/service.js diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 12d62d4..0913578 100644 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -6,6 +6,7 @@ + @@ -29,4 +30,4 @@ - \ No newline at end of file + diff --git a/src/main/webapp/meal-page-script.js b/src/main/webapp/meal-page-script.js index 489dd86..c593338 100644 --- a/src/main/webapp/meal-page-script.js +++ b/src/main/webapp/meal-page-script.js @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Alias the service. +// Use the `RealMealService` to actually query the backend. +const service = FakeMealService; + function fetchMealInfo() { // use mapping /meal.html?id= // fetches form server by action meal/ @@ -22,14 +26,7 @@ function fetchMealInfo() { window.location.replace("error.html"); } - fetch(`/meal/${id}`) - .then((response) => { - if (!response.ok) { - throw new Error("Network response was not ok"); - window.location.replace("error.html"); - } - return response.json(); - }) + service.getOne(id) .then((meal) => { const { title, description, ingredients, type } = meal; const titleElement = document.getElementById("title"); @@ -54,8 +51,7 @@ function redirectToSimilar() { const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const pageId = urlParams.get("id") ?? 0; - fetch(`/meal/similar?id=${pageId.toString()}`) - .then((response) => response.json()) + service.getSimilar(pageId.toString()) .then((id) => { const url = `/meal.html?id=${id.toString()}`; window.location.replace(url); diff --git a/src/main/webapp/meal.html b/src/main/webapp/meal.html index 2a9197c..dbfdd66 100644 --- a/src/main/webapp/meal.html +++ b/src/main/webapp/meal.html @@ -9,6 +9,7 @@ + @@ -39,4 +40,4 @@

Explore on a map:

- \ No newline at end of file + diff --git a/src/main/webapp/search-meal-script.js b/src/main/webapp/search-meal-script.js index 673b53a..e5e1a6a 100644 --- a/src/main/webapp/search-meal-script.js +++ b/src/main/webapp/search-meal-script.js @@ -12,10 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +// See a comment in meal-page-script.js +const service = FakeMealService; + function searchMeal() { const searchLine = getQueryParam("query"); - fetch(`/meal?query=${searchLine}`) - .then((response) => response.json()) + service.query(searchLine) .then((dishes) => { const amount = document.getElementById('amount-block'); const isSingular = dishes.length == 1; diff --git a/src/main/webapp/search-results.html b/src/main/webapp/search-results.html index a211cd5..3dc3c4c 100644 --- a/src/main/webapp/search-results.html +++ b/src/main/webapp/search-results.html @@ -7,6 +7,7 @@ + @@ -30,4 +31,4 @@ - \ No newline at end of file + diff --git a/src/main/webapp/service.js b/src/main/webapp/service.js new file mode 100644 index 0000000..15c8108 --- /dev/null +++ b/src/main/webapp/service.js @@ -0,0 +1,75 @@ +// This module exposes objects responsible for communicating with the backend. +// One "proper" version, talking to the real Java service, and one fake +// variant, returning hardcoded values, useful for local testing. +// +// All communication with the service should be done through one of the objects +// below. +// All exported objects should implement the same interface (the same set of +// public methods, each with the exact same signature). If we used TypeScript, +// we'd have defined an +// [interface](https://www.typescriptlang.org/docs/handbook/interfaces.html). +// +// NOTE(zajonc): the reason I've introduced this layer is because I wanted to +// detach the frontend part from the backend. +// Ideally, we'd like to be able to run a simple HTTP server serving the +// frontend, without the need to spin up Tomcat / Google App Engine. +// By mocking the service calls, we get rid of a strong dependency on the +// server. +// (TLDR: it's easier to test a Vue app this way). +// It's often a good idea to have a clear split between the frontend and the +// backend, though. In many cases they may even belong to separate +// repositories. You may even have multiple frontend apps for the same backend. + +// Builds HTTP requests and forwards them to the right endpoint. +// NOTE: I wasn't really able to test it - I might have messed something up here. +const RealMealService = { + getOne: (mealId) => + fetch(`/meal/${mealId}`).then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + window.location.replace("error.html"); + } + return response.json(); + }), + getSimilar: (mealId) => + fetch(`/meal/similar?id=${mealId}`).then((response) => response.json()), + query: (queryContent) => + fetch(`/meal?query=${queryContent}`).then((response) => response.json()), +}; + +// Fake variant, returning hardcoded values. +// No real network requests are made to the service. +// All methods return `Promise`s, for compatibility with the real service. +const FakeMealService = { + getOne: (mealId) => { + if (mealId == 1) { + return Promise.resolve(fakeMealA); + } else if (mealId == 2) { + return Promise.resolve(fakeMealB); + } else { + return Promise.resolve(null); + } + }, + // If current meal is A, suggest B. Suggest A otherwise. + getSimilar: (mealId) => + Promise.resolve(mealId == fakeMealA.id ? fakeMealB.id : fakeMealA.id), + // Return all known meals, regardless of the query. + query: (queryContent) => Promise.resolve([fakeMealA, fakeMealB]), +}; + +// NOTE(zajonc): I'm not too creative :/ +const fakeMealA = { + id: 1, + title: "MealA", + description: "MealADescription", + ingredients: ["Ingredient1, Ingredient2"], + type: "MealAType", +}; + +const fakeMealB = { + id: 2, + title: "MealB", + description: "MealBDescription", + ingredients: ["Ingredient3, Ingredient4"], + type: "MealBType", +}; From a57b7343388bc1baa8d46d17d2652f5266da00ab Mon Sep 17 00:00:00 2001 From: zajonc-gugle Date: Fri, 25 Dec 2020 17:07:25 +0100 Subject: [PATCH 02/18] Add a Vue app skeleton + a README --- src/main/webapp/vue-app/README.md | 53 + .../vue-app/meal-assistant-client/.gitignore | 23 + .../vue-app/meal-assistant-client/README.md | 29 + .../meal-assistant-client/babel.config.js | 3 + .../meal-assistant-client/package-lock.json | 11889 ++++++++++++++++ .../meal-assistant-client/package.json | 42 + .../meal-assistant-client/public/favicon.ico | Bin 0 -> 4286 bytes .../meal-assistant-client/public/index.html | 21 + .../vue-app/meal-assistant-client/src/App.vue | 28 + .../meal-assistant-client/src/assets/logo.png | Bin 0 -> 6849 bytes .../src/components/HelloWorld.vue | 58 + .../vue-app/meal-assistant-client/src/main.js | 8 + 12 files changed, 12154 insertions(+) create mode 100644 src/main/webapp/vue-app/README.md create mode 100644 src/main/webapp/vue-app/meal-assistant-client/.gitignore create mode 100644 src/main/webapp/vue-app/meal-assistant-client/README.md create mode 100644 src/main/webapp/vue-app/meal-assistant-client/babel.config.js create mode 100644 src/main/webapp/vue-app/meal-assistant-client/package-lock.json create mode 100644 src/main/webapp/vue-app/meal-assistant-client/package.json create mode 100644 src/main/webapp/vue-app/meal-assistant-client/public/favicon.ico create mode 100644 src/main/webapp/vue-app/meal-assistant-client/public/index.html create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/App.vue create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/assets/logo.png create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/components/HelloWorld.vue create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/main.js diff --git a/src/main/webapp/vue-app/README.md b/src/main/webapp/vue-app/README.md new file mode 100644 index 0000000..bb005a6 --- /dev/null +++ b/src/main/webapp/vue-app/README.md @@ -0,0 +1,53 @@ +# vue-app + +A simple rewrite of the basic functionality of the client, using Vue.js for all frontend code. +Some features are most likely missing. The main goal is to set up and use a usable environment +for working with modern frontend apps. + +## SPA + +The result is a [Single Page App](https://en.wikipedia.org/wiki/Single-page_application). + +The main difference between a SPA and a regular, oldschool app, is that there is only one html file +serving all subpages. It's content is dynamically rendered based on some magic JS code, usually provided +by a framework (here: Vue.js). + +## Development + +*Important* +To run any of the tools described below, you need to install [NodeJS](https://nodejs.org/en/) on your machine. + +From a developer's perspective, one of the key differences between the old approach (including js files in a +HTML file, which populate the global namespace) and the modern approach used in this directory, is the introduction +of a build step - the files we write are not the same files that are seen by the browser. + +A browser is able to render HTML files, which may define script dependencies as ` + + diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/assets/logo.png b/src/main/webapp/vue-app/meal-assistant-client/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d2503fc2a44b5053b0837ebea6e87a2d339a43 GIT binary patch literal 6849 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 +
+

{{ msg }}

+

+ For a guide and recipes on how to configure / customize this project,
+ check out the + vue-cli documentation. +

+

Installed CLI Plugins

+ +

Essential Links

+ +

Ecosystem

+ +
+ + + + + + diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/main.js b/src/main/webapp/vue-app/meal-assistant-client/src/main.js new file mode 100644 index 0000000..63eb05f --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/main.js @@ -0,0 +1,8 @@ +import Vue from 'vue' +import App from './App.vue' + +Vue.config.productionTip = false + +new Vue({ + render: h => h(App), +}).$mount('#app') From 42fdfb6f539216c0ce5f90d2f7c5d654d918c792 Mon Sep 17 00:00:00 2001 From: zajonc-gugle Date: Fri, 25 Dec 2020 17:43:30 +0100 Subject: [PATCH 03/18] Simple LandingPage in Vue --- src/main/webapp/script.js | 38 ------------ .../vue-app/meal-assistant-client/src/App.vue | 13 ++--- .../src/components/HelloWorld.vue | 58 ------------------- .../src/components/LandingPage.vue | 39 +++++++++++++ .../src/components/PhotoCopyright.vue | 10 ++++ .../src/components/README.md | 10 ++++ .../src/components/SearchBar.vue | 47 +++++++++++++++ .../meal-assistant-client/src/logic/README.md | 5 ++ .../src/logic/helpers.js | 23 ++++++++ .../vue-app/meal-assistant-client/src/main.js | 10 ++-- 10 files changed, 145 insertions(+), 108 deletions(-) delete mode 100644 src/main/webapp/script.js delete mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/components/HelloWorld.vue create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/components/LandingPage.vue create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/components/PhotoCopyright.vue create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/components/README.md create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/logic/README.md create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/logic/helpers.js diff --git a/src/main/webapp/script.js b/src/main/webapp/script.js deleted file mode 100644 index 4d803a5..0000000 --- a/src/main/webapp/script.js +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2020 Google LLC - -// 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 - -// https://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. - - -function createElementByTag(text, tag) { - const element = document.createElement(tag); - element.innerText = text; - return element; -} - -function capitalizeItems(string) { - return string - .split(' ') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); -} - -// solution from https://stackoverflow.com/questions/20174280/nodejs-convert-string-into-utf-8 -function encodingCheck(string) { - return JSON.parse(JSON.stringify(string)); -} - -function getQueryParam(param) { - const queryString = window.location.search; - const urlParams = new URLSearchParams(queryString); - return urlParams.get(param) ?? ""; -} diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/App.vue b/src/main/webapp/vue-app/meal-assistant-client/src/App.vue index 55df315..ba0ef4e 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/App.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/App.vue @@ -1,19 +1,18 @@ diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/LandingPage.vue b/src/main/webapp/vue-app/meal-assistant-client/src/components/LandingPage.vue new file mode 100644 index 0000000..c094a45 --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/components/LandingPage.vue @@ -0,0 +1,39 @@ + + + + + + diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/PhotoCopyright.vue b/src/main/webapp/vue-app/meal-assistant-client/src/components/PhotoCopyright.vue new file mode 100644 index 0000000..f3c176b --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/components/PhotoCopyright.vue @@ -0,0 +1,10 @@ + diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/README.md b/src/main/webapp/vue-app/meal-assistant-client/src/components/README.md new file mode 100644 index 0000000..648b4f2 --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/components/README.md @@ -0,0 +1,10 @@ +# Components + +Reusable Vue components. +As a rule of thumb, they should be kept small. If you find yourself writing some +complex functions as coponent methods, consider extracting the logic to a +standalone, pure-JS function and importing it into the component. + +Components can be tested by rendering them inside a browser-like environment. +[Vue testing library](https://testing-library.com/docs/vue-testing-library/intro/) +provides a set of utilities that make testing Vue components easy. diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue b/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue new file mode 100644 index 0000000..520736d --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue @@ -0,0 +1,47 @@ + + + diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/logic/README.md b/src/main/webapp/vue-app/meal-assistant-client/src/logic/README.md new file mode 100644 index 0000000..2c7d025 --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/logic/README.md @@ -0,0 +1,5 @@ +# Logic + +It is often useful to separate pure JS logic (functions and classes) from framework-specific components. + +The reason is the same as always - the simpler the module, the easier it is to maintain (== test). diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/logic/helpers.js b/src/main/webapp/vue-app/meal-assistant-client/src/logic/helpers.js new file mode 100644 index 0000000..581e405 --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/logic/helpers.js @@ -0,0 +1,23 @@ +export function createElementByTag(text, tag) { + const element = document.createElement(tag); + element.innerText = text; + return element; +} + +export function capitalizeItems(string) { + return string + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} + +// solution from https://stackoverflow.com/questions/20174280/nodejs-convert-string-into-utf-8 +export function encodingCheck(string) { + return JSON.parse(JSON.stringify(string)); +} + +export function getQueryParam(param) { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + return urlParams.get(param) ?? ""; +} diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/main.js b/src/main/webapp/vue-app/meal-assistant-client/src/main.js index 63eb05f..74f1ec5 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/main.js +++ b/src/main/webapp/vue-app/meal-assistant-client/src/main.js @@ -1,8 +1,8 @@ -import Vue from 'vue' -import App from './App.vue' +import Vue from "vue"; +import App from "./App.vue"; -Vue.config.productionTip = false +Vue.config.productionTip = false; new Vue({ - render: h => h(App), -}).$mount('#app') + render: (h) => h(App), +}).$mount("#app"); From 2eca685a9be64ef8e7f7e7fd8ff0141820576da7 Mon Sep 17 00:00:00 2001 From: zajonc-gugle Date: Fri, 25 Dec 2020 18:24:58 +0100 Subject: [PATCH 04/18] Introduce vue-router; rename LandingPage to Home. vue-router is responsible for Client Side Rendering in Vue apps. This bit is critical to having multiple urls handled by the same Single Page App - instead of trying to fetch a new html file for each url, the app will ask the router which component it should render. --- .../meal-assistant-client/package-lock.json | 5 +++ .../meal-assistant-client/package.json | 4 ++- .../vue-app/meal-assistant-client/src/App.vue | 34 ++++++++++++------- .../src/components/SearchBar.vue | 1 + .../vue-app/meal-assistant-client/src/main.js | 2 ++ .../meal-assistant-client/src/router/index.js | 28 +++++++++++++++ .../LandingPage.vue => views/Home.vue} | 19 +++-------- 7 files changed, 65 insertions(+), 28 deletions(-) create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/router/index.js rename src/main/webapp/vue-app/meal-assistant-client/src/{components/LandingPage.vue => views/Home.vue} (60%) diff --git a/src/main/webapp/vue-app/meal-assistant-client/package-lock.json b/src/main/webapp/vue-app/meal-assistant-client/package-lock.json index 0c02a34..7ab9c9e 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/package-lock.json +++ b/src/main/webapp/vue-app/meal-assistant-client/package-lock.json @@ -10995,6 +10995,11 @@ } } }, + "vue-router": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz", + "integrity": "sha512-CGAKWN44RqXW06oC+u4mPgHLQQi2t6vLD/JbGRDAXm0YpMv0bgpKuU5bBd7AvMgfTz9kXVRIWKHqRwGEb8xFkA==" + }, "vue-style-loader": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz", diff --git a/src/main/webapp/vue-app/meal-assistant-client/package.json b/src/main/webapp/vue-app/meal-assistant-client/package.json index 972818e..80d5d93 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/package.json +++ b/src/main/webapp/vue-app/meal-assistant-client/package.json @@ -9,11 +9,13 @@ }, "dependencies": { "core-js": "^3.6.5", - "vue": "^2.6.11" + "vue": "^2.6.11", + "vue-router": "^3.2.0" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0", + "@vue/cli-plugin-router": "^4.5.9", "@vue/cli-service": "~4.5.0", "babel-eslint": "^10.1.0", "eslint": "^6.7.2", diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/App.vue b/src/main/webapp/vue-app/meal-assistant-client/src/App.vue index ba0ef4e..c1b6326 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/App.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/App.vue @@ -1,20 +1,16 @@ + - - diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue b/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue index 520736d..23c6ecc 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue @@ -25,6 +25,7 @@ export default { methods: { submit: function () { const searchLine = document.getElementById("query").value; + // TODO: refactor to use vue-router compatible navigation window.location.replace( `search-results.html?query=${encodeURIComponent(searchLine)}` ); diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/main.js b/src/main/webapp/vue-app/meal-assistant-client/src/main.js index 74f1ec5..71702ed 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/main.js +++ b/src/main/webapp/vue-app/meal-assistant-client/src/main.js @@ -1,8 +1,10 @@ import Vue from "vue"; import App from "./App.vue"; +import router from "./router"; Vue.config.productionTip = false; new Vue({ + router, render: (h) => h(App), }).$mount("#app"); diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/router/index.js b/src/main/webapp/vue-app/meal-assistant-client/src/router/index.js new file mode 100644 index 0000000..d8047b7 --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/router/index.js @@ -0,0 +1,28 @@ +import Vue from "vue"; +import VueRouter from "vue-router"; +import Home from "../views/Home.vue"; + +Vue.use(VueRouter); + +// A list of views, each associated with a path. +// The router will look at the current path (e.g. /meal/4) and render the first matching +// route whose `path` matches the url. +// See https://stackoverflow.com/a/10473302/7742560 for a brief explanation of Client Side Routing. +// TODO: define remaining routes. +const routes = [ + { + path: "/", + name: "Home", + component: Home, + }, +]; + +// Some vue-router boilerplate. +// NOTE(zajonc): to be honest, I don't really know what this bit does. It's been autogenerated. +const router = new VueRouter({ + mode: "history", + base: process.env.BASE_URL, + routes, +}); + +export default router; diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/LandingPage.vue b/src/main/webapp/vue-app/meal-assistant-client/src/views/Home.vue similarity index 60% rename from src/main/webapp/vue-app/meal-assistant-client/src/components/LandingPage.vue rename to src/main/webapp/vue-app/meal-assistant-client/src/views/Home.vue index c094a45..044592b 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/components/LandingPage.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/views/Home.vue @@ -1,5 +1,5 @@ - - - From 3dd9a05de6a2220e93e37b47b7f0998bb645712d Mon Sep 17 00:00:00 2001 From: zajonc-gugle Date: Fri, 25 Dec 2020 19:15:57 +0100 Subject: [PATCH 05/18] SearchResults view, Vue-style. - redirect to SearchResults after submitting the form - fetch data on query update - render returned results --- .../src/components/SearchBar.vue | 5 +- .../src/logic/service.js | 74 +++++++++++++++++++ .../meal-assistant-client/src/router/index.js | 6 ++ .../src/views/SearchResults.vue | 71 ++++++++++++++++++ 4 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/logic/service.js create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/views/SearchResults.vue diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue b/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue index 23c6ecc..6539efc 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue @@ -25,9 +25,8 @@ export default { methods: { submit: function () { const searchLine = document.getElementById("query").value; - // TODO: refactor to use vue-router compatible navigation - window.location.replace( - `search-results.html?query=${encodeURIComponent(searchLine)}` + this.$router.push( + `search-results?query=${encodeURIComponent(searchLine)}` ); }, // `methods` can be used to render dynamic values in a template, based on the injected properties diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/logic/service.js b/src/main/webapp/vue-app/meal-assistant-client/src/logic/service.js new file mode 100644 index 0000000..bcfb45c --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/logic/service.js @@ -0,0 +1,74 @@ +// This module exposes objects responsible for communicating with the backend. +// One "proper" version, talking to the real Java service, and one fake +// variant, returning hardcoded values, useful for local testing. +// +// All communication with the service should be done through one of the objects +// below. +// All exported objects should implement the same interface (the same set of +// public methods, each with the exact same signature). If we used TypeScript, +// we'd have defined an +// [interface](https://www.typescriptlang.org/docs/handbook/interfaces.html). +// +// NOTE(zajonc): the reason I've introduced this layer is because I wanted to +// detach the frontend part from the backend. +// Ideally, we'd like to be able to run a simple HTTP server serving the +// frontend, without the need to spin up Tomcat / Google App Engine. +// By mocking the service calls, we get rid of a strong dependency on the +// server. +// (TLDR: it's easier to test a Vue app this way). +// It's often a good idea to have a clear split between the frontend and the +// backend, though. In many cases they may even belong to separate +// repositories. You may even have multiple frontend apps for the same backend. + +// Builds HTTP requests and forwards them to the right endpoint. +// NOTE: I wasn't really able to test it - I might have messed something up here. +export const RealMealService = { + getOne: (mealId) => + fetch(`/meal/${mealId}`).then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + }), + getSimilar: (mealId) => + fetch(`/meal/similar?id=${mealId}`).then((response) => response.json()), + query: (queryContent) => + fetch(`/meal?query=${queryContent}`).then((response) => response.json()), +}; + +// Fake variant, returning hardcoded values. +// No real network requests are made to the service. +// All methods return `Promise`s, for compatibility with the real service. +export const FakeMealService = { + getOne: (mealId) => { + if (mealId == 1) { + return Promise.resolve(fakeMealA); + } else if (mealId == 2) { + return Promise.resolve(fakeMealB); + } else { + return Promise.resolve(null); + } + }, + // If current meal is A, suggest B. Suggest A otherwise. + getSimilar: (mealId) => + Promise.resolve(mealId == fakeMealA.id ? fakeMealB.id : fakeMealA.id), + // Return all known meals, regardless of the query. + query: () => Promise.resolve([fakeMealA, fakeMealB]), +}; + +// NOTE(zajonc): I'm not too creative :/ +const fakeMealA = { + id: 1, + title: "MealA", + description: "MealADescription", + ingredients: ["Ingredient1, Ingredient2"], + type: "MealAType", +}; + +const fakeMealB = { + id: 2, + title: "MealB", + description: "MealBDescription", + ingredients: ["Ingredient3, Ingredient4"], + type: "MealBType", +}; diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/router/index.js b/src/main/webapp/vue-app/meal-assistant-client/src/router/index.js index d8047b7..de3ea17 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/router/index.js +++ b/src/main/webapp/vue-app/meal-assistant-client/src/router/index.js @@ -1,6 +1,7 @@ import Vue from "vue"; import VueRouter from "vue-router"; import Home from "../views/Home.vue"; +import SearchResults from "../views/SearchResults.vue"; Vue.use(VueRouter); @@ -10,6 +11,11 @@ Vue.use(VueRouter); // See https://stackoverflow.com/a/10473302/7742560 for a brief explanation of Client Side Routing. // TODO: define remaining routes. const routes = [ + { + path: "/search-results/", + name: "SearchResults", + component: SearchResults, + }, { path: "/", name: "Home", diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/views/SearchResults.vue b/src/main/webapp/vue-app/meal-assistant-client/src/views/SearchResults.vue new file mode 100644 index 0000000..c284efc --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/views/SearchResults.vue @@ -0,0 +1,71 @@ + + + From d004839c1a96a8f3e7167e7cad2ae1e3a29707a8 Mon Sep 17 00:00:00 2001 From: zajonc-gugle Date: Sun, 27 Dec 2020 10:37:03 +0100 Subject: [PATCH 06/18] Fix ingredients in FakeService definitions --- .../webapp/vue-app/meal-assistant-client/src/logic/service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/logic/service.js b/src/main/webapp/vue-app/meal-assistant-client/src/logic/service.js index bcfb45c..7cc6789 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/logic/service.js +++ b/src/main/webapp/vue-app/meal-assistant-client/src/logic/service.js @@ -61,7 +61,7 @@ const fakeMealA = { id: 1, title: "MealA", description: "MealADescription", - ingredients: ["Ingredient1, Ingredient2"], + ingredients: ["Ingredient1", "Ingredient2"], type: "MealAType", }; @@ -69,6 +69,6 @@ const fakeMealB = { id: 2, title: "MealB", description: "MealBDescription", - ingredients: ["Ingredient3, Ingredient4"], + ingredients: ["Ingredient3", "Ingredient4"], type: "MealBType", }; From e74dfd3ea28213a695f47fb5738c7a8d28cb1d7a Mon Sep 17 00:00:00 2001 From: zajonc-gugle Date: Sun, 27 Dec 2020 10:38:25 +0100 Subject: [PATCH 07/18] MealDetail page --- .../src/components/Map.vue | 33 +++++++++ .../src/components/ShowSimilarMealButton.vue | 24 +++++++ .../meal-assistant-client/src/router/index.js | 9 +++ .../src/views/MealDetail.vue | 70 +++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/components/Map.vue create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/components/ShowSimilarMealButton.vue create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/views/MealDetail.vue diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/Map.vue b/src/main/webapp/vue-app/meal-assistant-client/src/components/Map.vue new file mode 100644 index 0000000..1992453 --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/components/Map.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/ShowSimilarMealButton.vue b/src/main/webapp/vue-app/meal-assistant-client/src/components/ShowSimilarMealButton.vue new file mode 100644 index 0000000..f78bade --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/components/ShowSimilarMealButton.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/router/index.js b/src/main/webapp/vue-app/meal-assistant-client/src/router/index.js index de3ea17..16bb83f 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/router/index.js +++ b/src/main/webapp/vue-app/meal-assistant-client/src/router/index.js @@ -2,6 +2,7 @@ import Vue from "vue"; import VueRouter from "vue-router"; import Home from "../views/Home.vue"; import SearchResults from "../views/SearchResults.vue"; +import MealDetail from "../views/MealDetail.vue"; Vue.use(VueRouter); @@ -11,6 +12,14 @@ Vue.use(VueRouter); // See https://stackoverflow.com/a/10473302/7742560 for a brief explanation of Client Side Routing. // TODO: define remaining routes. const routes = [ + { + // The `:id` will automagically read the remaining part of the url and + // assign it to a variable stored under `$route.params.id`. + // For example, if we enter /meal/5, id will be set to 5. + path: "/meal/:id", + name: "MealDetail", + component: MealDetail, + }, { path: "/search-results/", name: "SearchResults", diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/views/MealDetail.vue b/src/main/webapp/vue-app/meal-assistant-client/src/views/MealDetail.vue new file mode 100644 index 0000000..24ece85 --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/views/MealDetail.vue @@ -0,0 +1,70 @@ + + + From 02a7b4387cbf61027327b5cd567b5b4d62674e07 Mon Sep 17 00:00:00 2001 From: zajonc-gugle Date: Sun, 27 Dec 2020 13:23:46 +0100 Subject: [PATCH 08/18] Bind onClick action to ShowSimilarMealButton --- .../src/components/ShowSimilarMealButton.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/ShowSimilarMealButton.vue b/src/main/webapp/vue-app/meal-assistant-client/src/components/ShowSimilarMealButton.vue index f78bade..3260944 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/components/ShowSimilarMealButton.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/components/ShowSimilarMealButton.vue @@ -1,5 +1,7 @@ + + diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/logic/map_utils.js b/src/main/webapp/vue-app/meal-assistant-client/src/logic/map_utils.js new file mode 100644 index 0000000..bb9c8ca --- /dev/null +++ b/src/main/webapp/vue-app/meal-assistant-client/src/logic/map_utils.js @@ -0,0 +1,113 @@ +// Creates a map and adds it to the page. +// +// NOTE(zajonc): I've reused the existing code for simplicity. +// However, since we're using a modern framework, we could've simply used an existing library +// that solves all the manual stuff and exposes an easy to use API. +// [vue2-google-maps](https://www.npmjs.com/package/vue2-google-maps) seems like an interesting option. +// +// The JS ecosystem is massive. Whenever you try to use some popular tool (like Google Maps) from +// within a popular framework (like Vue), there's a huge chance you can reuse some library to make your +// life easier. +// This is possible because of the added build step - we can simply download dependencies from npm and bundle +// them into our final script. +// +// NOTE(zajonc): I've added an `element` argument to make the function a bit easier to use. In general, it's +// a good idea to avoid looking things up in the global state (here: `document.getElementById`) in helper functions. +// You can always pass things as arguments. +export function createMap(type, element) { + const userLocation = new google.maps.LatLng(55.746514, 37.627022); + const mapOptions = { + zoom: 14, + center: userLocation, + }; + const map = new google.maps.Map(element, mapOptions); + const locationMarker = new google.maps.Marker({ + position: userLocation, + map, + title: "You are here (probably)", + label: "You", + animation: google.maps.Animation.DROP, + }); + + // Geolocation warning will be retujrned from the promise. + // If is not a critical error, therefore we do not reject the promise. + let geoWarning = null; + + return getCurrentPositionPromise() + .then((position) => { + const location = new google.maps.LatLng( + position.coords.latitude, + position.coords.longitude + ); + map.setCenter(location); + locationMarker.setPosition(location); + }) + .catch((err) => { + if (err === "NO_GEOLOCATION") { + geoWarning = "Your browser doesn't support geolocation."; + } else if (err === "GET_POSITION_FAILED") { + geoWarning = + "The Geolocation service failed. Share your location, please."; + } + }) + .then(() => { + const request = { + location: map.getCenter(), + radius: "1500", + type: ["restaurant", "cafe", "meal_takeaway", "bar", "bakery"], + query: type, + }; + const service = new google.maps.places.PlacesService(map); + return getSearchPromise(service, request, map).then((results) => { + addRestaurants(results, map); + }); + }) + .catch((err) => { + console.log(`Caught error: ${err}`); + }) + .finally(() => geoWarning); +} + +function addRestaurants(results, map) { + for (const result of results) { + createMarker(result, map); + } +} + +function getSearchPromise(service, request) { + return new Promise((resolve, reject) => { + const onSuccess = (results, status) => { + if (status !== "OK") { + reject("FAILED"); + } else { + resolve(results); + } + }; + const onError = () => reject("FAILED"); + service.textSearch(request, onSuccess, onError); + }); +} + +function getCurrentPositionPromise() { + return new Promise((resolve, reject) => { + if (!navigator.geolocation) { + reject("NO_GEOLOCATION"); + } else { + const onSuccess = (position) => resolve(position); + const onError = () => reject("GET_POSITION_FAILED"); + navigator.geolocation.getCurrentPosition(onSuccess, onError); + } + }); +} + +function createMarker(place, map) { + const marker = new google.maps.Marker({ + map, + position: place.geometry.location, + title: place.name, + }); + const infoWindow = new google.maps.InfoWindow({ content: place.name }); + marker.addListener("click", () => { + infoWindow.open(map, marker); + }); +} diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/main.js b/src/main/webapp/vue-app/meal-assistant-client/src/main.js index 71702ed..63c70d8 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/main.js +++ b/src/main/webapp/vue-app/meal-assistant-client/src/main.js @@ -1,10 +1,22 @@ import Vue from "vue"; import App from "./App.vue"; import router from "./router"; +import LoadScript from "vue-plugin-load-script"; + +Vue.use(LoadScript); Vue.config.productionTip = false; -new Vue({ - router, - render: (h) => h(App), -}).$mount("#app"); +// NOTE(zajonc): do not include the key - it's inactive anyway (as the time of writing). +// Requests to the places API will, unfortunately, fail, but the map will load correctly. +// That should do for UI purposes. +Vue.loadScript("https://maps.googleapis.com/maps/api/js?libraries=places") + .catch((err) => { + console.error(`Failed fetching google maps script: ${err}`); + }) + .then(() => { + new Vue({ + router, + render: (h) => h(App), + }).$mount("#app"); + }); From bd0387831cd5359ec4d6d0b1636d2e8c366f7eb2 Mon Sep 17 00:00:00 2001 From: zajonc-gugle Date: Sun, 27 Dec 2020 13:33:00 +0100 Subject: [PATCH 10/18] Update readme with installation instructions --- src/main/webapp/vue-app/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/vue-app/README.md b/src/main/webapp/vue-app/README.md index bb005a6..e921d28 100644 --- a/src/main/webapp/vue-app/README.md +++ b/src/main/webapp/vue-app/README.md @@ -49,5 +49,6 @@ the app. ### TLDR ``` cd meal-assistant-client -npm run serve +npm run install # install all dependencies. They'll be installed to node_modules/. +npm run serve # compile the bundle and start a local server. The app will be accessible at localhost:8080 ``` From 8a42148c445e69a0f324787f9623859aaff15f1b Mon Sep 17 00:00:00 2001 From: zajonc-gugle Date: Sun, 27 Dec 2020 13:45:54 +0100 Subject: [PATCH 11/18] Define layout once, in App.vue --- .../vue-app/meal-assistant-client/src/App.vue | 45 ++++++++++++++++++- .../meal-assistant-client/src/views/Home.vue | 23 +--------- .../src/views/MealDetail.vue | 41 ++++++----------- .../src/views/SearchResults.vue | 39 +++++----------- 4 files changed, 69 insertions(+), 79 deletions(-) diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/App.vue b/src/main/webapp/vue-app/meal-assistant-client/src/App.vue index c1b6326..3ec6a71 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/App.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/App.vue @@ -1,16 +1,57 @@ + + diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/Map.vue b/src/main/webapp/vue-app/meal-assistant-client/src/components/Map.vue index a857af8..a35ce0d 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/components/Map.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/components/Map.vue @@ -44,7 +44,7 @@ export default { #map { border: thin solid black; height: 500px; - width: 80%; padding-bottom: 10px; + margin: auto; } diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue b/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue index 96c6f20..f9a223d 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue @@ -47,3 +47,20 @@ export default { }, }; + + diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/views/Home.vue b/src/main/webapp/vue-app/meal-assistant-client/src/views/Home.vue index e576290..96b1d5e 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/views/Home.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/views/Home.vue @@ -7,3 +7,10 @@ export default { name: "Home", }; + + diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/views/MealDetail.vue b/src/main/webapp/vue-app/meal-assistant-client/src/views/MealDetail.vue index 232555d..dfe16d1 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/views/MealDetail.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/views/MealDetail.vue @@ -13,7 +13,9 @@

Explore on a map:

- +
+ +
Waiting for the results...
@@ -55,3 +57,9 @@ export default { }, }; + + diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/views/SearchResults.vue b/src/main/webapp/vue-app/meal-assistant-client/src/views/SearchResults.vue index 2a7dda6..de601da 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/views/SearchResults.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/views/SearchResults.vue @@ -52,3 +52,9 @@ export default { }, }; + + From ad33f209afe0a1dc1e76b94d721ef0a3e5dd5123 Mon Sep 17 00:00:00 2001 From: zajonc-gugle Date: Sun, 27 Dec 2020 22:08:48 +0100 Subject: [PATCH 16/18] Fix router.push() arguments --- .../vue-app/meal-assistant-client/src/components/SearchBar.vue | 2 +- .../src/components/ShowSimilarMealButton.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue b/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue index f9a223d..a00ba04 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/components/SearchBar.vue @@ -28,7 +28,7 @@ export default { submit: function () { const searchLine = document.getElementById("query").value; this.$router.push( - `search-results?query=${encodeURIComponent(searchLine)}` + `/search-results?query=${encodeURIComponent(searchLine)}` ); }, // `methods` can be used to render dynamic values in a template, based on the injected properties diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/components/ShowSimilarMealButton.vue b/src/main/webapp/vue-app/meal-assistant-client/src/components/ShowSimilarMealButton.vue index 3260944..334ceaa 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/components/ShowSimilarMealButton.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/components/ShowSimilarMealButton.vue @@ -18,7 +18,7 @@ export default { redirectToSimilar: function () { const service = FakeMealService; service.getSimilar(this.currentMealId).then((similarMealId) => { - this.$router.push(`meal/${similarMealId}`); + this.$router.push(`/meal/${similarMealId}`); }); }, }, From 761c204056999be69f63f4acf90b4bb1f17b46c0 Mon Sep 17 00:00:00 2001 From: zajonc-gugle Date: Mon, 28 Dec 2020 20:36:40 +0100 Subject: [PATCH 17/18] Extract Header to a separate component. Home uses a slighlty different layout - the search bar is located below below the text, because it's the main element on the page. In all other views the search bar functions as a navigation element, so we put it on the very top. --- .../vue-app/meal-assistant-client/src/App.vue | 19 --------- .../src/components/Header.vue | 40 +++++++++++++++++++ .../meal-assistant-client/src/views/Home.vue | 13 +++++- .../src/views/MealDetail.vue | 3 ++ .../src/views/SearchResults.vue | 3 ++ 5 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 src/main/webapp/vue-app/meal-assistant-client/src/components/Header.vue diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/App.vue b/src/main/webapp/vue-app/meal-assistant-client/src/App.vue index c9501c0..b30df23 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/App.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/App.vue @@ -8,23 +8,6 @@
- - - @@ -40,14 +23,12 @@ + + diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/views/Home.vue b/src/main/webapp/vue-app/meal-assistant-client/src/views/Home.vue index 96b1d5e..23e207e 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/views/Home.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/views/Home.vue @@ -1,16 +1,25 @@ diff --git a/src/main/webapp/vue-app/meal-assistant-client/src/views/MealDetail.vue b/src/main/webapp/vue-app/meal-assistant-client/src/views/MealDetail.vue index dfe16d1..da1d334 100644 --- a/src/main/webapp/vue-app/meal-assistant-client/src/views/MealDetail.vue +++ b/src/main/webapp/vue-app/meal-assistant-client/src/views/MealDetail.vue @@ -1,5 +1,6 @@