diff --git a/.gitignore b/.gitignore index 627d9c74..0a30e391 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,7 @@ npm-debug.log # The directory NPM downloads your dependencies sources to. /assets/node_modules/ - +/assets/package-lock.json # Since we are building assets from assets/, # we ignore priv/static. You may want to comment # this depending on your deployment strategy. @@ -43,4 +43,7 @@ npm-debug.log # VS code elixir_ls folder see https://github.com/JakeBecker/vscode-elixir-ls .elixir_ls -.env \ No newline at end of file +.env + +#elm +/assets/elm-stuff diff --git a/assets/elm.json b/assets/elm.json new file mode 100644 index 00000000..8a812b68 --- /dev/null +++ b/assets/elm.json @@ -0,0 +1,27 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/browser": "1.0.2", + "elm/core": "1.0.4", + "elm/html": "1.0.0", + "elm/http": "2.0.0", + "elm/json": "1.1.3" + }, + "indirect": { + "elm/bytes": "1.0.8", + "elm/file": "1.0.5", + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/assets/js/app.js b/assets/js/app.js index 8a5d386f..9e28157e 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -15,3 +15,10 @@ import "phoenix_html" // // Local files can be imported directly using relative paths, for example: // import socket from "./socket" + +// import Elm application +import { Elm } from "../src/Main.elm"; + +var app = Elm.Main.init({ + node: document.getElementById('elm-app') +}); \ No newline at end of file diff --git a/assets/package-lock.json b/assets/package-lock.json index 2fb640a3..16fd0ca0 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -3550,6 +3550,212 @@ "minimalistic-crypto-utils": "^1.0.0" } }, + "elm-webpack-loader": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/elm-webpack-loader/-/elm-webpack-loader-6.0.1.tgz", + "integrity": "sha512-SkltiDAbegaiPxsQq7sxqTyRUXzD4n6UaT9JTZXIYoAjAEi/ogaIbYLQD6Ne5ZaSW3JOo6S/vb+VO5v0qDdirw==", + "dev": true, + "requires": { + "glob": "^7.1.1", + "loader-utils": "^1.0.2", + "node-elm-compiler": "^5.0.0", + "yargs": "^6.5.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "^1.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yargs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^4.2.0" + } + }, + "yargs-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "dev": true, + "requires": { + "camelcase": "^3.0.0" + } + } + } + }, "emojis-list": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", @@ -4007,6 +4213,16 @@ "pkg-dir": "^3.0.0" } }, + "find-elm-dependencies": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/find-elm-dependencies/-/find-elm-dependencies-2.0.2.tgz", + "integrity": "sha512-nM5UCbccD1G8CGK2GsM7ykG3ksOAl9E+34jiDfl07CAl2OPnLpBVWY2hlxEmIkSBfdJjSopEowWHrO0cI8RhxQ==", + "dev": true, + "requires": { + "firstline": "1.2.0", + "lodash": "4.17.15" + } + }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -4025,6 +4241,12 @@ "readable-stream": "^2.0.2" } }, + "firstline": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/firstline/-/firstline-1.2.0.tgz", + "integrity": "sha1-yfSIbn9/vwr8EtcZQdzgaxkq6gU=", + "dev": true + }, "flatten": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz", @@ -4336,7 +4558,6 @@ "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4355,7 +4576,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4449,7 +4669,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4535,8 +4754,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -4642,8 +4860,7 @@ "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -6451,6 +6668,38 @@ "integrity": "sha1-VfuN62mQcHB/tn+RpGDwRIKUx30=", "dev": true }, + "node-elm-compiler": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/node-elm-compiler/-/node-elm-compiler-5.0.4.tgz", + "integrity": "sha512-VQsT8QSierYGkHzRed+b4MnccQVF1+qPHunE8jBoU7jD6YpuRqCDPzEoC2zfyEJS80qVnlMZrqobLnyjzX9lJg==", + "dev": true, + "requires": { + "cross-spawn": "6.0.5", + "find-elm-dependencies": "2.0.2", + "lodash": "4.17.15", + "temp": "^0.9.0" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "temp": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.1.tgz", + "integrity": "sha512-WMuOgiua1xb5R56lE0eH6ivpVmg/lq2OHm4+LtT/xtEtPQ+sz6N3bBM6WZ5FvO1lO4IKIOb43qnhoc4qxP5OeA==", + "dev": true, + "requires": { + "rimraf": "~2.6.2" + } + } + } + }, "node-libs-browser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", diff --git a/assets/package.json b/assets/package.json index a4ba0e7a..543ff315 100644 --- a/assets/package.json +++ b/assets/package.json @@ -15,6 +15,7 @@ "babel-loader": "^8.0.0", "copy-webpack-plugin": "^4.5.0", "css-loader": "^2.1.1", + "elm-webpack-loader": "^6.0.1", "mini-css-extract-plugin": "^0.4.0", "optimize-css-assets-webpack-plugin": "^4.0.0", "uglifyjs-webpack-plugin": "^1.2.4", diff --git a/assets/src/Main.elm b/assets/src/Main.elm new file mode 100644 index 00000000..a20a962e --- /dev/null +++ b/assets/src/Main.elm @@ -0,0 +1,103 @@ +module Main exposing (main) + +import Browser +import Html exposing (Html, button, div, p, text, textarea) +import Html.Attributes exposing (class, value) +import Html.Events exposing (onClick, onInput) +import Http exposing (..) +import Json.Decode as JD +import Json.Encode as JE + + +main = + Browser.element + { init = init + , update = update + , subscriptions = subscriptions + , view = view + } + + + +-- Model containg the capture text + + +type alias Model = + { capture : String, message : String } + + + +-- Msg + + +type Msg + = Capture String + | CreateCapture + | SaveCaptureResult (Result Http.Error String) + + + +-- init + + +initModel : Model +initModel = + { capture = "", message = "" } + + +type alias Flags = + () + + +init : Flags -> ( Model, Cmd Msg ) +init _ = + ( initModel, Cmd.none ) + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + Capture text -> + ( { model | capture = text }, Cmd.none ) + + CreateCapture -> + ( model, saveCapture model.capture ) + + SaveCaptureResult (Ok response) -> + ( { model | capture = "", message = "Capture saved" }, Cmd.none ) + + SaveCaptureResult (Err e) -> + ( { model | message = "The capture couldn't be saved" }, Cmd.none ) + + +saveCapture : String -> Cmd Msg +saveCapture capture = + Http.post + { url = "/api/captures/create" + , body = Http.jsonBody (captureEncode capture) + , expect = Http.expectJson SaveCaptureResult captureDecoder + } + + +captureEncode : String -> JE.Value +captureEncode capture = + JE.object [ ( "text", JE.string capture ) ] + + +captureDecoder : JD.Decoder String +captureDecoder = + JD.field "text" JD.string + + +subscriptions : Model -> Sub Msg +subscriptions _ = + Sub.none + + +view : Model -> Html Msg +view model = + div [] + [ p [] [ text model.message ] + , textarea [ onInput Capture, value model.capture ] [] + , button [ class "db", onClick CreateCapture ] [ text "Save capture" ] + ] diff --git a/assets/static/images/favicon.ico b/assets/static/images/favicon.ico new file mode 100644 index 00000000..d7057bd0 Binary files /dev/null and b/assets/static/images/favicon.ico differ diff --git a/assets/static/js/service_worker.js b/assets/static/js/service_worker.js new file mode 100644 index 00000000..57f793ac --- /dev/null +++ b/assets/static/js/service_worker.js @@ -0,0 +1,25 @@ +self.addEventListener('install', function(e) { + e.waitUntil( + caches.open('progessive-elm').then(function(cache) { + return cache.addAll([ + '/elm/capture', + ]); + }) + ); + }); + + self.addEventListener('fetch', function(event) { + console.log('Service Worker Intercept: ' + event.request.url); + + event.respondWith( + + caches.match(event.request).then(function(response) { + + console.log('Service Worker Serve: ' + event.request.url); + + return response || fetch(event.request); + + }) + + ); + }); \ No newline at end of file diff --git a/assets/static/manifest.json b/assets/static/manifest.json new file mode 100644 index 00000000..162c5aa5 --- /dev/null +++ b/assets/static/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "DWYL App", + "name": "Spike using progressive web app", + "icons": [ + { + "src": "/images/favicon.ico", + "sizes": "192x192", + "type": "image/png" + } + ], + "start_url": "/", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" + } \ No newline at end of file diff --git a/assets/webpack.config.js b/assets/webpack.config.js index 63c1d208..d050011c 100644 --- a/assets/webpack.config.js +++ b/assets/webpack.config.js @@ -31,6 +31,14 @@ module.exports = (env, options) => ({ { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] + }, + { + test: /\.elm$/, + exclude: [/elm-stuff/, /node_modules/], + use: { + loader: 'elm-webpack-loader', + options: {} + } } ] }, diff --git a/lib/app_web/controllers/captures_controller.ex b/lib/app_web/controllers/captures_controller.ex index be2e00f9..48537da0 100644 --- a/lib/app_web/controllers/captures_controller.ex +++ b/lib/app_web/controllers/captures_controller.ex @@ -20,4 +20,26 @@ defmodule AppWeb.CaptureController do render(conn, "new.html", changeset: changeset) end end + + def api_create(conn, %{"text" => capture}) do + case Ctx.create_item(%{text: capture}) do + {:ok, item} -> + render(conn, "capture.json", item: %{text: item.text}) + + {:error, _} -> + error = %{error: "The capture cannot be saved."} + render(conn, "capture_error.json", err: error) + end + end + + def api_create(conn, _params) do + error = %{error: "text field is not defined"} + render(conn, "capture_error.json", err: error) + end + + # Returns html where the Elm application + # is initialised + def init_elm(conn, _params) do + render(conn, "init_elm.html") + end end diff --git a/lib/app_web/controllers/item_controller.ex b/lib/app_web/controllers/item_controller.ex index 86664e13..918cba13 100644 --- a/lib/app_web/controllers/item_controller.ex +++ b/lib/app_web/controllers/item_controller.ex @@ -9,6 +9,11 @@ defmodule AppWeb.ItemController do render(conn, "index.html", items: items) end + def api_index(conn, _params) do + items = Ctx.list_items() + render(conn, "index.json", items: items) + end + def new(conn, _params) do changeset = Ctx.change_item(%Item{}) render(conn, "new.html", changeset: changeset) diff --git a/lib/app_web/endpoint.ex b/lib/app_web/endpoint.ex index 2fe6aa9b..b8c0afd6 100644 --- a/lib/app_web/endpoint.ex +++ b/lib/app_web/endpoint.ex @@ -13,7 +13,7 @@ defmodule AppWeb.Endpoint do at: "/", from: :app, gzip: false, - only: ~w(css fonts images js favicon.ico robots.txt) + only: ~w(css fonts images js favicon.ico robots.txt cache_manifest.json service_worker.js) # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. diff --git a/lib/app_web/router.ex b/lib/app_web/router.ex index be17849d..f85880c4 100644 --- a/lib/app_web/router.ex +++ b/lib/app_web/router.ex @@ -25,6 +25,9 @@ defmodule AppWeb.Router do get "/", PageController, :index post "/register", PageController, :register get "/auth/google/callback", GoogleAuthController, :index + + # Endpoint to initialise Elm application + get "/elm/capture", CaptureController, :init_elm end scope "/", AppWeb do @@ -50,7 +53,10 @@ defmodule AppWeb.Router do end # Other scopes may use custom stacks. - # scope "/api", AppWeb do - # pipe_through :api - # end + scope "/api", AppWeb do + pipe_through :api + + post "/captures/create", CaptureController, :api_create + get "/items", ItemController, :api_index + end end diff --git a/lib/app_web/templates/capture/init_elm.html.eex b/lib/app_web/templates/capture/init_elm.html.eex new file mode 100644 index 00000000..d409f797 --- /dev/null +++ b/lib/app_web/templates/capture/init_elm.html.eex @@ -0,0 +1,3 @@ +

Elm PWA

+ +
diff --git a/lib/app_web/templates/layout/app.html.eex b/lib/app_web/templates/layout/app.html.eex index 0516576c..726f6ac2 100644 --- a/lib/app_web/templates/layout/app.html.eex +++ b/lib/app_web/templates/layout/app.html.eex @@ -22,6 +22,7 @@ +
@@ -35,5 +36,12 @@ <%= render @view_module, @view_template, assigns %> + diff --git a/lib/app_web/views/capture_view.ex b/lib/app_web/views/capture_view.ex index 6733f855..594b73ef 100644 --- a/lib/app_web/views/capture_view.ex +++ b/lib/app_web/views/capture_view.ex @@ -1,3 +1,11 @@ defmodule AppWeb.CaptureView do use AppWeb, :view + + def render("capture.json", data) do + data.item + end + + def render("capture_error.json", data) do + data.err + end end diff --git a/lib/app_web/views/item_view.ex b/lib/app_web/views/item_view.ex index 84240e42..f74b1d91 100644 --- a/lib/app_web/views/item_view.ex +++ b/lib/app_web/views/item_view.ex @@ -1,3 +1,7 @@ defmodule AppWeb.ItemView do use AppWeb, :view + + def render("index.json", data) do + Enum.map(data.items, fn i -> %{id: i.id, text: i.text} end) + end end diff --git a/test/app_web/controllers/capture_controller_test.exs b/test/app_web/controllers/capture_controller_test.exs index b3469226..c2b25ad6 100644 --- a/test/app_web/controllers/capture_controller_test.exs +++ b/test/app_web/controllers/capture_controller_test.exs @@ -22,11 +22,22 @@ defmodule AppWeb.CaptureControllerTest do assert redirected_to(conn) == Routes.categorise_path(conn, :index) end + + test "api - create a new capture", %{conn: conn} do + conn = post(conn, Routes.capture_path(conn, :api_create), item: @create_attrs) + assert json_response(conn, 200) + end + test "renders errors when data is invalid", %{conn: conn} do conn = post(conn, Routes.capture_path(conn, :create), item: @invalid_attrs) assert html_response(conn, 200) =~ "Capture" end + + test "elm - render capture html page", %{conn: conn} do + conn = get(conn, Routes.capture_path(conn, :init_elm)) + assert html_response(conn, 200) + end end end diff --git a/test/app_web/controllers/item_controller_test.exs b/test/app_web/controllers/item_controller_test.exs index f416a5ae..21c87cac 100644 --- a/test/app_web/controllers/item_controller_test.exs +++ b/test/app_web/controllers/item_controller_test.exs @@ -19,6 +19,11 @@ defmodule AppWeb.ItemControllerTest do conn = get(conn, Routes.item_path(conn, :index)) assert html_response(conn, 200) =~ "Listing Items" end + + test "api - lists all items", %{conn: conn} do + conn = get(conn, Routes.item_path(conn, :api_index)) + assert json_response(conn, 200) + end end describe "new item" do