diff --git a/.formatter.exs b/.formatter.exs
new file mode 100644
index 00000000..8a6391c6
--- /dev/null
+++ b/.formatter.exs
@@ -0,0 +1,5 @@
+[
+ import_deps: [:ecto, :phoenix],
+ inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
+ subdirectories: ["priv/*/migrations"]
+]
diff --git a/.gitignore b/.gitignore
index 076d05cd..4d1577b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,11 +4,23 @@
/deps
/*.ez
+# Where 3rd-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# Ignore package tarball (built via "mix hex.build").
+app-*.tar
+
# Generated on crash by the VM
erl_crash.dump
# Static artifacts
-/node_modules
+node_modules
+node_modules/*
+npm-debug.log
+assets/package-lock.json
# Since we are building assets from web/static,
# we ignore priv/static. You may want to comment
@@ -21,7 +33,7 @@ erl_crash.dump
# Alternatively, you may comment the line below and commit the
# secrets file as long as you replace its contents by environment
# variables.
-/config/prod.secret.exs
+# /config/prod.secret.exs
lib-cov
*.seed
@@ -36,9 +48,8 @@ pids
logs
results
-npm-debug.log
-node_modules
.DS_Store
.vagrant
test/coverage.html
coverage/
+cover/
diff --git a/.travis.yml b/.travis.yml
index 136dcb9b..b472f501 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,22 @@
-language: node_js
-node_js:
- - 0.12
+language: elixir
+elixir:
+ - 1.10.3
+otp_release:
+ - 22.1.8
services:
- - elasticsearch
+ - postgresql
+cache:
+ directories:
+ - _build
+ - deps
+script:
+ - mix do setup, compile --warnings-as-errors, coveralls.json
+after_success:
+- bash <(curl -s https://codecov.io/bash)
env:
- - ES_HOST="127.0.0.1" ES_PORT=9200 ES_INDEX=travis JWT_SECRET="EverythingIsAwesome!"
+ global:
+ - MIX_ENV=test
+cache:
+ directories:
+ - _build
+ - deps
diff --git a/Procfile b/Procfile
index 489b2700..2271dc84 100644
--- a/Procfile
+++ b/Procfile
@@ -1 +1 @@
-web: node server.js
+web: MIX_ENV=prod mix do ecto.migrate, phx.server
diff --git a/README.md b/README.md
index bcb2abe4..19731595 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,2112 @@
-# Phoenix Todo List Tutorial!
+
+# Phoenix Todo List Tutorial
+A complete beginners step-by-step tutorial
+for building a Todo List in Phoenix.
+100% functional. 0% JavaScript. Just `HTML`, `CSS` and `Elixir`.
+Fast and maintainable.
-
+[](https://travis-ci.org/dwyl/phoenix-todo-list-tutorial)
+[](http://codecov.io/github/dwyl/phoenix-todo-list-tutorial?branch=master)
+[](http://hits.dwyl.com/dwyl/phoenix-todo-list-tutorial)
+[](https://github.com/dwyl/phoenix-todo-list-tutorial/issues)
+
+
-Comming Soon!!
+## Why? 🤷
+
+Todo lists are familiar to most people;
+we make lists all the time.
+_Building_ a Todo list from scratch is a great way to learn Elixir/Phoenix
+because the UI/UX is simple,
+so we can focus on implementation.
+
+For the team
+[**`@dwyl`**](https://github.com/dwyl)
+this app/tutorial
+is a showcase of how server side rendering
+(_with client side progressive enhancement_)
+can provide a excellent balance between
+developer effectiveness (_shipping features fast_),
+UX and _accessibility_.
+The server rendered pages take less than 5ms to respond
+so the UX is _fast_.
+On Heroku (_after the "Free" App wakes up!_),
+round-trip response times are sub 100ms for all interactions,
+so it _feels_ like a client-side rendered App.
+
+
+
+
+## What? 💭
+
+A Todo list tutorial
+that shows a complete beginner
+how to build an app in Elixir/Phoenix
+from scratch.
+
+
+### Try it on Heroku: [phxtodo.herokuapp.com](https://phxtodo.herokuapp.com)
+
+Try the Heroku version.
+Add a few items to the list and test the functionality.
+
+
+
+Even with a full HTTP round-trip for each interaction,
+the response time is _fast_.
+Pay attention to how Chrome|Firefox|Safari
+waits for the response from the server before re-rendering the page.
+The old full page refresh of yesteryear is _gone_.
+Modern browsers intelligently render just the changes!
+So the UX approximates "native"!
+Seriously, try the Heroku app on your Phone and see!
+
+
+### TodoMVC
+
+In this tutorial
+we are using the
+[TodoMVC](https://github.com/dwyl/javascript-todo-list-tutorial#todomvc)
+CSS to simplify our UI.
+This has several advantages
+the biggest being _minimising_ how much CSS we have to write!
+It also means we have a guide to which _features_
+need to be implemented to achieve full functionality.
+
+> **Note**: we _love_ `CSS` for its incredible power/flexibility,
+but we know that not everyone like it.
+see: [learn-tachyons#why](https://github.com/dwyl/learn-tachyons#why)
+The _last_ thing we want is to waste tons of time
+with `CSS` in a `Phoenix` tutorial!
+
+
+
+
+## Who? 👤
+
+This tutorial is for
+anyone who is learning to Elixir/Phoenix.
+No prior experience with Phoenix is assumed/expected.
+We have included _all_ the steps required to build the app.
+
+> If you get stuck on any step,
+please open an
+[issue](https://github.com/dwyl/phoenix-todo-list-tutorial/issues)
+on GitHub where we are happy to help you get unstuck!
+If you feel that any line of code can use a bit more explanation/clarity,
+please don't hesitate to _inform_ us!
+We _know_ what it's like to be a beginner,
+it can be _frustrating_ when something does not make sense!
+If you're stuck, don't suffer in silence,
+asking questions on GitHub
+helps _everyone_ to learn!
+
+
+
+
+## _How_? 👩💻
+
+
+### Before You Start! 💡
+
+_Before_ you attempt to _build_ the Todo List,
+make sure you have everything you need installed on you computer.
+See:
+[prerequisites](https://github.com/dwyl/phoenix-chat-example#0-pre-requisites-before-you-start)
+
+Once you have confirmed that you have Phoenix & PostgreSQL installed,
+try running the _finished_ App.
+
+
+### 0. Run The _Finished_ App on Your `localhost` 💻
+
+_Before_ you start building your own version of the Todo List App,
+run the _finished_ version on your `localhost`
+to confirm that it works.
+
+Clone the project from GitHub:
+
+```sh
+git clone git@github.com:dwyl/phoenix-todo-list-tutorial.git && cd phoenix-todo-list-tutorial
+```
+
+Install dependencies and setup the database:
+
+```sh
+mix setup
+```
+
+Start the Phoenix server:
+
+```sh
+mix phx.server
+```
+
+Visit
+[`localhost:4000`](http://localhost:4000)
+in your web browser.
+
+
+You should see:
+
+
+
+Now that you have the _finished_ example app
+running on your `localhost`,
+let's build it from scratch
+and understand all the steps.
+
+If you ran the finished app on your `localhost`
+(_and you really should!_),
+you will need to change up a directory before starting the tutorial:
+
+```
+cd ..
+```
+
+Now you are ready to _build_!
+
+
+
+### 1. Create a New Phoenix Project 🆕
+
+In your terminal,
+create a new Phoenix app
+using the following
+[`mix`](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html)
+command:
+
+```sh
+mix phx.new app
+```
+
+When prompted to install dependencies,
+type Y followed by Enter.
+
+Change into the newly created `app` directory (`cd app`)
+and
+ensure you have everything you need:
+
+```sh
+mix setup
+```
+
+Start the Phoenix server:
+
+```sh
+mix phx.server
+```
+
+Now you can visit
+[`localhost:4000`](http://localhost:4000)
+in your web browser.
+You should see something similar to:
+
+
+
+Shut down the Phoenix server ctrl+C.
+
+Run the tests to ensure everything works as expected:
+
+```sh
+mix test
+```
+
+You should see:
+
+```sh
+Compiling 16 files (.ex)
+Generated app app
+
+17:49:40.111 [info] Already up
+...
+
+Finished in 0.04 seconds
+3 tests, 0 failures
+```
+
+Having established that the Phoenix App works as expected,
+let's move on to creating some files!
+
+
+
+### 2. Create `items` Schema
+
+In creating a basic Todo List we only need one schema: `items`.
+Later we can add separate lists and tags to organise/categorise
+our `items` but for now this is all we need.
+
+Run the following [generator](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Html.html) command to create the items table:
+
+```sh
+mix phx.gen.html Todo Item items text:string person_id:integer status:integer
+```
+
+Strictly speaking we only _need_ the `text` and `status` fields,
+but since we know we want to associate items with people
+(_later in the tutorial),
+we are adding the field _now_.
+
+
+You will see the following output:
+
+```
+* creating lib/app_web/controllers/item_controller.ex
+* creating lib/app_web/templates/item/edit.html.eex
+* creating lib/app_web/templates/item/form.html.eex
+* creating lib/app_web/templates/item/index.html.eex
+* creating lib/app_web/templates/item/new.html.eex
+* creating lib/app_web/templates/item/show.html.eex
+* creating lib/app_web/views/item_view.ex
+* creating test/app_web/controllers/item_controller_test.exs
+* creating lib/app/todo/item.ex
+* creating priv/repo/migrations/20200521145424_create_items.exs
+* creating lib/app/todo.ex
+* injecting lib/app/todo.ex
+* creating test/app/todo_test.exs
+* injecting test/app/todo_test.exs
+
+Add the resource to your browser scope in lib/app_web/router.ex:
+
+ resources "/items", ItemController
+
+
+Remember to update your repository by running migrations:
+
+ $ mix ecto.migrate
+```
+
+That created a _bunch_ of files!
+Some of which we don't strictly _need_.
+We could _manually_ create _only_ the files we _need_,
+but this is the "official" way of creating a CRUD App in Phoenix,
+so we are using it for speed.
+
+
+> **Note**: Phoenix
+[Contexts](https://hexdocs.pm/phoenix/contexts.html)
+denoted in this example as `Todo`,
+are "_dedicated modules that expose and group related functionality_."
+We feel they _unnecessarily complicate_ basic Phoenix Apps
+with layers of "interface" and we _really_ wish we could
+[avoid](https://github.com/phoenixframework/phoenix/issues/3832) them.
+But given that they are baked into the generators,
+and the _creator_ of the framework
+[_likes_](https://youtu.be/tMO28ar0lW8?t=376) them,
+we have a choice: either get on board with Contexts
+or _manually_ create all the files in our Phoenix projects.
+Generators are a _much_ faster way to build!
+_Embrace_ them,
+even if you end up having to `delete` a few
+unused files along the way!
+
+We are _not_ going to explain each of these files
+at this stage in the tutorial because
+it's _easier_ to understand the files
+as you are _building_ the App!
+The purpose of each file will become clear
+as you progress through editing them.
+
+
+
+
+
+### 2.1 Add the `/items` Resources to `router.ex`
+
+Follow the instructions noted by the generator to
+add the `resources "/items", ItemController` to the `router.ex`.
+
+Open the `lib/app_web/router.ex` file
+and locate the line: `scope "/", AppWeb do`
+Add the line to the end of the block.
+e.g:
+
+```elixir
+scope "/", AppWeb do
+ pipe_through :browser
+
+ get "/", PageController, :index
+ resources "/items", ItemController # this is the new line
+end
+```
+
+Your `router.ex` file should look like this:
+[`router.ex#L20`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/f66184b58b7dd1ef593680e7a1a446247909cae7/lib/app_web/router.ex#L20)
+
+
+
+### 2.2 _Run_ The App!
+
+At this point we _already_ have a functional Todo List
+(_if we were willing to use the default Phoenix UI_).
+Try running the app on your `localhost`:
+Run the generated migrations with `mix ecto.migrate` then the server with:
+```
+mix phx.server
+```
+
+Visit: http://localhost:4000/items/new
+and input some data.
+
+
+
+Click the "Save" button
+and you will be redirected to the "show" page:
+[/items/1](http://localhost:4000/items/1)
+
+
+
+This is not an attractive User Experience (UX),
+but it _works_!
+Here is a _list_ of items; a "Todo List":
+
+
+
+
+Let's improve the UX by using the TodoMVC `HTML` and `CSS`!
+
+
+
+### 3. Create the TodoMVC UI/UX
+
+To recreate the TodoMVC UI/UX,
+let's borrow the `HTML` code directly from the example.
+
+Visit: http://todomvc.com/examples/vanillajs
+add a couple of items to the list,
+then inspect the source
+using your browser's
+[Dev Tools](https://developers.google.com/web/tools/chrome-devtools/open).
+e.g:
+
+
+
+Right-click on the source you want
+(e.g: ``)
+ and select "Edit as HTML":
+
+
+
+
+Once the `HTML` for the `` is editable,
+select it and copy it.
+
+
+
+
+The `HTML` code is:
+
+
+```html
+
+
+
todos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Let's convert this `HTML` to an Embedded Elixir
+([`EEx`](https://hexdocs.pm/eex/EEx.html)) template.
+
+
+> **Note**: the _reason_ that we are copying this `HTML`
+from the browser's Elements inspector
+instead of _directly_ from the source
+on GitHub:
+[`examples/vanillajs/index.html`](https://github.com/tastejs/todomvc/blob/c50cc922495fd76cb44844e3b1cd77e35a5d6be1/examples/vanillajs/index.html#L18)
+is that this is a "single page app",
+so the `
`
+only gets populated in the browser.
+Copying it from the browser Dev Tools
+is the easiest way to get the _complete_ `HTML`.
+
+
+
+### 3.1 Paste the HTML into `index.html.eex`
+
+Open the `lib/app_web/templates/item/index.html.eex` file
+and scroll to the bottom.
+
+Then (_without removing the code that is already there_)
+paste the `HTML` code we sourced from TodoMVC.
+
+> e.g:
+[`/lib/app_web/templates/item/index.html.eex#L33-L73`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/bddacda93ecd892fe0907210bab335e6b6e5e489/lib/app_web/templates/item/index.html.eex#L33-L73)
+
+If you attempt to run the app now
+and visit
+[http://localhost:4000/items/](http://localhost:4000/items/)
+You will see this (_without the TodoMVC `CSS`_):
+
+
+
+That's obviously not what we want,
+so let's get the TodoMVC `CSS`
+and save it in our project!
+
+
+
+### 3.2 Save the TodoMVC CSS to `/assets/css`
+
+Visit
+http://todomvc.com/examples/vanillajs/node_modules/todomvc-app-css/index.css
+and save the file to `/assets/css/todomvc-app.css`.
+
+e.g:
+[`/assets/css/todomvc-app.css`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/65bec23b92307527a414f77b667b29ea10619e5a/assets/css/todomvc-app.css)
+
+
+
+### 3.3 Import the `todomvc-app.css` in `app.scss`
+
+Open the `assets/css/app.scss` file and replace it with the following:
+
+```css
+/* This file is for your main application css. */
+/* @import "./phoenix.css"; */
+@import "./todomvc-app.css";
+```
+
+e.g:
+[`/assets/css/app.scss#L3`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/5fd673089be616c9bb783277ae2d4f0e310b8413/assets/css/app.scss#L3)
+
+We commented out the
+`@import "./phoenix.css";`
+because we don't want the Phoenix (Milligram) styles
+_conflicting_ with the TodoMVC ones.
+
+
+
+### 3.4 _Simplify_ The Layout Template
+
+Open your `lib/app_web/templates/layout/app.html.eex` file
+and replace the contents with the following code:
+
+```html
+
+
+
+
+
+
+ Phoenix Todo List
+ "/>
+
+
+
+ <%= @inner_content %>
+
+
+
+
+```
+
+
+> Before:
+[`/lib/app_web/templates/layout/app.html.eex`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/bddacda93ecd892fe0907210bab335e6b6e5e489/lib/app_web/templates/layout/app.html.eex)
+> After:
+[`/lib/app_web/templates/layout/app.html.eex#L12`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/4d9e2031687d07494f98fad407dc5cc2be795b24/lib/app_web/templates/layout/app.html.eex#L12)
+
+`<%= @inner_content %>` is where the Todo App will be rendered.
+
+> **Note**: the `
//
-// You will need to verify the user token in the "connect/2" function
-// in "web/channels/user_socket.ex":
+// You will need to verify the user token in the "connect/3" function
+// in "lib/web/channels/user_socket.ex":
//
-// def connect(%{"token" => token}, socket) do
+// def connect(%{"token" => token}, socket, _connect_info) do
// # max_age: 1209600 is equivalent to two weeks in seconds
// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
// {:ok, user_id} ->
@@ -48,9 +51,7 @@ let socket = new Socket("/socket", {params: {token: window.userToken}})
// end
// end
//
-// Finally, pass the token on connect as below. Or remove it
-// from connect if you don't care about authentication.
-
+// Finally, connect to the socket:
socket.connect()
// Now that you are connected, you can join channels with a topic:
diff --git a/assets/package.json b/assets/package.json
new file mode 100644
index 00000000..53c5abc2
--- /dev/null
+++ b/assets/package.json
@@ -0,0 +1,33 @@
+{
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/dwyl/phoenix-todo-list-tutorial.git"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "description": "A Phoenix Implementation of TodoMVC",
+ "license": "MIT",
+ "scripts": {
+ "deploy": "webpack --mode production",
+ "watch": "webpack --mode development --watch"
+ },
+ "dependencies": {
+ "phoenix": "file:../deps/phoenix",
+ "phoenix_html": "file:../deps/phoenix_html"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.0.0",
+ "@babel/preset-env": "^7.0.0",
+ "babel-loader": "^8.0.0",
+ "copy-webpack-plugin": "^5.1.1",
+ "css-loader": "^3.4.2",
+ "sass-loader": "^8.0.2",
+ "node-sass": "^4.13.1",
+ "mini-css-extract-plugin": "^0.9.0",
+ "optimize-css-assets-webpack-plugin": "^5.0.1",
+ "terser-webpack-plugin": "^2.3.2",
+ "webpack": "4.41.5",
+ "webpack-cli": "^3.3.2"
+ }
+}
diff --git a/web/static/assets/favicon.ico b/assets/static/favicon.ico
similarity index 100%
rename from web/static/assets/favicon.ico
rename to assets/static/favicon.ico
diff --git a/web/static/assets/images/phoenix.png b/assets/static/images/phoenix.png
similarity index 100%
rename from web/static/assets/images/phoenix.png
rename to assets/static/images/phoenix.png
diff --git a/web/static/assets/robots.txt b/assets/static/robots.txt
similarity index 100%
rename from web/static/assets/robots.txt
rename to assets/static/robots.txt
diff --git a/assets/webpack.config.js b/assets/webpack.config.js
new file mode 100644
index 00000000..dd77c3dd
--- /dev/null
+++ b/assets/webpack.config.js
@@ -0,0 +1,51 @@
+const path = require('path');
+const glob = require('glob');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const TerserPlugin = require('terser-webpack-plugin');
+const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+
+module.exports = (env, options) => {
+ const devMode = options.mode !== 'production';
+
+ return {
+ optimization: {
+ minimizer: [
+ new TerserPlugin({ cache: true, parallel: true, sourceMap: devMode }),
+ new OptimizeCSSAssetsPlugin({})
+ ]
+ },
+ entry: {
+ 'app': glob.sync('./vendor/**/*.js').concat(['./js/app.js'])
+ },
+ output: {
+ filename: '[name].js',
+ path: path.resolve(__dirname, '../priv/static/js'),
+ publicPath: '/js/'
+ },
+ devtool: devMode ? 'source-map' : undefined,
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ use: {
+ loader: 'babel-loader'
+ }
+ },
+ {
+ test: /\.[s]?css$/,
+ use: [
+ MiniCssExtractPlugin.loader,
+ 'css-loader',
+ 'sass-loader',
+ ],
+ }
+ ]
+ },
+ plugins: [
+ new MiniCssExtractPlugin({ filename: '../css/app.css' }),
+ new CopyWebpackPlugin([{ from: 'static/', to: '../' }])
+ ]
+ }
+};
diff --git a/brunch-config.js b/brunch-config.js
deleted file mode 100644
index d2fe679c..00000000
--- a/brunch-config.js
+++ /dev/null
@@ -1,69 +0,0 @@
-exports.config = {
- // See http://brunch.io/#documentation for docs.
- files: {
- javascripts: {
- joinTo: "js/app.js"
-
- // To use a separate vendor.js bundle, specify two files path
- // http://brunch.io/docs/config#-files-
- // joinTo: {
- // "js/app.js": /^(web\/static\/js)/,
- // "js/vendor.js": /^(web\/static\/vendor)|(deps)/
- // }
- //
- // To change the order of concatenation of files, explicitly mention here
- // order: {
- // before: [
- // "web/static/vendor/js/jquery-2.1.1.js",
- // "web/static/vendor/js/bootstrap.min.js"
- // ]
- // }
- },
- stylesheets: {
- joinTo: "css/app.css",
- order: {
- after: ["web/static/css/app.css"] // concat app.css last
- }
- },
- templates: {
- joinTo: "js/app.js"
- }
- },
-
- conventions: {
- // This option sets where we should place non-css and non-js assets in.
- // By default, we set this to "/web/static/assets". Files in this directory
- // will be copied to `paths.public`, which is "priv/static" by default.
- assets: /^(web\/static\/assets)/
- },
-
- // Phoenix paths configuration
- paths: {
- // Dependencies and current project directories to watch
- watched: [
- "web/static",
- "test/static"
- ],
-
- // Where to compile files to
- public: "priv/static"
- },
-
- // Configure your plugins
- plugins: {
- babel: {
- // Do not use ES6 compiler in vendor code
- ignore: [/web\/static\/vendor/]
- }
- },
-
- modules: {
- autoRequire: {
- "js/app.js": ["web/static/js/app"]
- }
- },
-
- npm: {
- enabled: true
- }
-};
diff --git a/config/config.exs b/config/config.exs
index e6956bbf..84643cb3 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -3,25 +3,29 @@
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
-use Mix.Config
# General application configuration
-config :api,
- ecto_repos: [Api.Repo]
+use Mix.Config
+
+config :app,
+ ecto_repos: [App.Repo]
# Configures the endpoint
-config :api, Api.Endpoint,
+config :app, AppWeb.Endpoint,
url: [host: "localhost"],
- secret_key_base: "20vNsXrlE/ySWLOy2TocheXQVcOh3n3KfPndZTdTy+KKUYkmJZOUcBt4NaV2WcvD",
- render_errors: [view: Api.ErrorView, accepts: ~w(html json)],
- pubsub: [name: Api.PubSub,
- adapter: Phoenix.PubSub.PG2]
+ secret_key_base: "/dFtq9nub4hRMOK5Bbaqvrw6yglP5P5qS3aZXqUv+rizrkA4lSItOpIwGOboSeOt",
+ render_errors: [view: AppWeb.ErrorView, accepts: ~w(html json), layout: false],
+ pubsub_server: App.PubSub,
+ live_view: [signing_salt: "x2TlvXSe"]
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
+# Use Jason for JSON parsing in Phoenix
+config :phoenix, :json_library, Jason
+
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
-import_config "#{Mix.env}.exs"
+import_config "#{Mix.env()}.exs"
diff --git a/config/dev.exs b/config/dev.exs
index 44a853fd..fe4d0d6f 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -1,28 +1,67 @@
use Mix.Config
+# Configure your database
+config :app, App.Repo,
+ username: "postgres",
+ password: "postgres",
+ database: "app_dev",
+ hostname: "localhost",
+ show_sensitive_data_on_connection_error: true,
+ pool_size: 10
+
# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
-# with brunch.io to recompile .js and .css sources.
-config :api, Api.Endpoint,
+# with webpack to recompile .js and .css sources.
+config :app, AppWeb.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
check_origin: false,
- watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
- cd: Path.expand("../", __DIR__)]]
+ watchers: [
+ node: [
+ "node_modules/webpack/bin/webpack.js",
+ "--mode",
+ "development",
+ "--watch-stdin",
+ cd: Path.expand("../assets", __DIR__)
+ ]
+ ]
+# ## SSL Support
+#
+# In order to use HTTPS in development, a self-signed
+# certificate can be generated by running the following
+# Mix task:
+#
+# mix phx.gen.cert
+#
+# Note that this task requires Erlang/OTP 20 or later.
+# Run `mix help phx.gen.cert` for more information.
+#
+# The `http:` config above can be replaced with:
+#
+# https: [
+# port: 4001,
+# cipher_suite: :strong,
+# keyfile: "priv/cert/selfsigned_key.pem",
+# certfile: "priv/cert/selfsigned.pem"
+# ],
+#
+# If desired, both `http:` and `https:` keys can be
+# configured to run both http and https servers on
+# different ports.
# Watch static and templates for browser reloading.
-config :api, Api.Endpoint,
+config :app, AppWeb.Endpoint,
live_reload: [
patterns: [
- ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
- ~r{priv/gettext/.*(po)$},
- ~r{web/views/.*(ex)$},
- ~r{web/templates/.*(eex)$}
+ ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"priv/gettext/.*(po)$",
+ ~r"lib/app_web/(live|views)/.*(ex)$",
+ ~r"lib/app_web/templates/.*(eex)$"
]
]
@@ -33,11 +72,5 @@ config :logger, :console, format: "[$level] $message\n"
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20
-# Configure your database
-config :api, Api.Repo,
- adapter: Ecto.Adapters.Postgres,
- username: "postgres",
- password: "postgres",
- database: "api_dev",
- hostname: "localhost",
- pool_size: 10
+# Initialize plugs at runtime for faster development compilation
+config :phoenix, :plug_init_mode, :runtime
diff --git a/config/prod.exs b/config/prod.exs
index 90349256..2abc3503 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -1,20 +1,17 @@
use Mix.Config
-# For production, we configure the host to read the PORT
-# from the system environment. Therefore, you will need
-# to set PORT=80 before running your server.
+# For production, don't forget to configure the url host
+# to something meaningful, Phoenix uses this information
+# when generating URLs.
#
-# You should also configure the url host to something
-# meaningful, we use this information when generating URLs.
-#
-# Finally, we also include the path to a manifest
+# Note we also include the path to a cache manifest
# containing the digested version of static files. This
-# manifest is generated by the mix phoenix.digest task
-# which you typically run after static files are built.
-config :api, Api.Endpoint,
- http: [port: {:system, "PORT"}],
+# manifest is generated by the `mix phx.digest` task,
+# which you should run after static files are built and
+# before starting your production server.
+config :app, AppWeb.Endpoint,
url: [host: "example.com", port: 80],
- cache_static_manifest: "priv/static/manifest.json"
+ cache_static_manifest: "priv/static/cache_manifest.json"
# Do not print debug messages in production
config :logger, level: :info
@@ -24,38 +21,35 @@ config :logger, level: :info
# To get SSL working, you will need to add the `https` key
# to the previous section and set your `:url` port to 443:
#
-# config :api, Api.Endpoint,
+# config :app, AppWeb.Endpoint,
# ...
# url: [host: "example.com", port: 443],
-# https: [port: 443,
-# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
-# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
-#
-# Where those two env variables return an absolute path to
-# the key and cert in disk or a relative path inside priv,
-# for example "priv/ssl/server.key".
-#
-# We also recommend setting `force_ssl`, ensuring no data is
-# ever sent via http, always redirecting to https:
-#
-# config :api, Api.Endpoint,
+# https: [
+# port: 443,
+# cipher_suite: :strong,
+# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+# certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
+# transport_options: [socket_opts: [:inet6]]
+# ]
+#
+# The `cipher_suite` is set to `:strong` to support only the
+# latest and more secure SSL ciphers. This means old browsers
+# and clients may not be supported. You can set it to
+# `:compatible` for wider support.
+#
+# `:keyfile` and `:certfile` expect an absolute path to the key
+# and cert in disk or a relative path inside priv, for example
+# "priv/ssl/server.key". For all supported SSL configuration
+# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
+#
+# We also recommend setting `force_ssl` in your endpoint, ensuring
+# no data is ever sent via http, always redirecting to https:
+#
+# config :app, AppWeb.Endpoint,
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.
-# ## Using releases
-#
-# If you are doing OTP releases, you need to instruct Phoenix
-# to start the server for all endpoints:
-#
-# config :phoenix, :serve_endpoints, true
-#
-# Alternatively, you can configure exactly which server to
-# start per endpoint:
-#
-# config :api, Api.Endpoint, server: true
-#
-
-# Finally import the config/prod.secret.exs
-# which should be versioned separately.
+# Finally import the config/prod.secret.exs which loads secrets
+# and configuration from environment variables.
import_config "prod.secret.exs"
diff --git a/config/prod.secret.exs b/config/prod.secret.exs
new file mode 100644
index 00000000..595dc2a6
--- /dev/null
+++ b/config/prod.secret.exs
@@ -0,0 +1,41 @@
+# In this file, we load production configuration and secrets
+# from environment variables. You can also hardcode secrets,
+# although such is generally not recommended and you have to
+# remember to add this file to your .gitignore.
+use Mix.Config
+
+database_url =
+ System.get_env("DATABASE_URL") ||
+ raise """
+ environment variable DATABASE_URL is missing.
+ For example: ecto://USER:PASS@HOST/DATABASE
+ """
+
+config :app, App.Repo,
+ # ssl: true,
+ url: database_url,
+ pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
+
+secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+config :app, AppWeb.Endpoint,
+ http: [
+ port: String.to_integer(System.get_env("PORT") || "4000"),
+ transport_options: [socket_opts: [:inet6]]
+ ],
+ secret_key_base: secret_key_base
+
+# ## Using releases (Elixir v1.9+)
+#
+# If you are doing OTP releases, you need to instruct Phoenix
+# to start each relevant endpoint:
+#
+# config :app, AppWeb.Endpoint, server: true
+#
+# Then you can assemble a release by calling `mix release`.
+# See `mix help release` for more information.
diff --git a/config/test.exs b/config/test.exs
index 5ece7c95..7068d024 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -1,19 +1,22 @@
use Mix.Config
+# Configure your database
+#
+# The MIX_TEST_PARTITION environment variable can be used
+# to provide built-in test partitioning in CI environment.
+# Run `mix help test` for more information.
+config :app, App.Repo,
+ username: "postgres",
+ password: "postgres",
+ database: "app_test#{System.get_env("MIX_TEST_PARTITION")}",
+ hostname: "localhost",
+ pool: Ecto.Adapters.SQL.Sandbox
+
# We don't run a server during test. If one is required,
# you can enable the server option below.
-config :api, Api.Endpoint,
- http: [port: 4001],
+config :app, AppWeb.Endpoint,
+ http: [port: 4002],
server: false
# Print only warnings and errors during test
config :logger, level: :warn
-
-# Configure your database
-config :api, Api.Repo,
- adapter: Ecto.Adapters.Postgres,
- username: "postgres",
- password: "postgres",
- database: "api_test",
- hostname: "localhost",
- pool: Ecto.Adapters.SQL.Sandbox
diff --git a/coveralls.json b/coveralls.json
new file mode 100644
index 00000000..cca3ffa9
--- /dev/null
+++ b/coveralls.json
@@ -0,0 +1,13 @@
+{
+ "coverage_options": {
+ "minimum_coverage": 100
+ },
+ "skip_files": [
+ "lib/app/application.ex",
+ "lib/app_web.ex",
+ "lib/app_web/router.ex",
+ "lib/app_web/views/error_helpers.ex",
+ "test/",
+ "lib/app_web/telemetry.ex"
+ ]
+}
diff --git a/elixir_buildpack.config b/elixir_buildpack.config
new file mode 100644
index 00000000..e650d238
--- /dev/null
+++ b/elixir_buildpack.config
@@ -0,0 +1,8 @@
+# Elixir version
+elixir_version=1.10
+
+# Erlang version
+# available versions https://github.com/HashNuke/heroku-buildpack-elixir-otp-builds/blob/master/otp-versions
+erlang_version=22.2.7
+
+# always_rebuild=true
diff --git a/email_templates/welcome_html.html b/email_templates/welcome_html.html
deleted file mode 100644
index 4b212940..00000000
--- a/email_templates/welcome_html.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
Hi!
-
-
Welcome to dwyl! Thanks so much for your interest, we're excited to have you!
-
-
At dwyl we work to help you do what you love (whether you know what that is yet or not). We do this through technology and we hold certain beliefs strongly, including that your data belongs to you and no one else.
-
-
As we develop dwyl over the summer, you'll be the first to hear of the progress and the first to use the app.
-
-
If you have any questions or suggestions, please reply to this email or add an issue to github (if that's your kind of thing), we really appreciate it!
-
-
Happy dwyling!
-Ines & Nelson
\ No newline at end of file
diff --git a/email_templates/welcome_text.txt b/email_templates/welcome_text.txt
deleted file mode 100644
index 43e78c43..00000000
--- a/email_templates/welcome_text.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-Hi!
-
-Welcome to dwyl! Thanks so much for your interest, we're excited to have you!
-
-At dwyl we work to help you do what you love (whether you know what that is yet or not). We do this through technology and we hold certain beliefs strongly, including that *your data belongs to you* and no one else. You can see those here: http://git.io/vtYgs
-
-As we develop dwyl, you'll be the first to hear of the progress and the first to use the app.
-
-If you have any questions or suggestions, please reply to this email or add an issue to github here (if that's your kind of thing): http://git.io/vtYgz
-We really appreciate it!
-
-Happy dwyling,
-Ines & Nelson
diff --git a/lib/api.ex b/lib/api.ex
deleted file mode 100644
index 27ddd603..00000000
--- a/lib/api.ex
+++ /dev/null
@@ -1,31 +0,0 @@
-defmodule Api do
- use Application
-
- # See http://elixir-lang.org/docs/stable/elixir/Application.html
- # for more information on OTP Applications
- def start(_type, _args) do
- import Supervisor.Spec
-
- # Define workers and child supervisors to be supervised
- children = [
- # Start the Ecto repository
- supervisor(Api.Repo, []),
- # Start the endpoint when the application starts
- supervisor(Api.Endpoint, []),
- # Start your own worker by calling: Api.Worker.start_link(arg1, arg2, arg3)
- # worker(Api.Worker, [arg1, arg2, arg3]),
- ]
-
- # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
- # for other strategies and supported options
- opts = [strategy: :one_for_one, name: Api.Supervisor]
- Supervisor.start_link(children, opts)
- end
-
- # Tell Phoenix to update the endpoint configuration
- # whenever the application is updated.
- def config_change(changed, _new, removed) do
- Api.Endpoint.config_change(changed, removed)
- :ok
- end
-end
diff --git a/lib/api/repo.ex b/lib/api/repo.ex
deleted file mode 100644
index 2f94f3b5..00000000
--- a/lib/api/repo.ex
+++ /dev/null
@@ -1,3 +0,0 @@
-defmodule Api.Repo do
- use Ecto.Repo, otp_app: :api
-end
diff --git a/lib/app.ex b/lib/app.ex
new file mode 100644
index 00000000..a10dc060
--- /dev/null
+++ b/lib/app.ex
@@ -0,0 +1,9 @@
+defmodule App do
+ @moduledoc """
+ App keeps the contexts that define your domain
+ and business logic.
+
+ Contexts are also responsible for managing your data, regardless
+ if it comes from the database, an external API or others.
+ """
+end
diff --git a/lib/app/application.ex b/lib/app/application.ex
new file mode 100644
index 00000000..cd46ff43
--- /dev/null
+++ b/lib/app/application.ex
@@ -0,0 +1,34 @@
+defmodule App.Application do
+ # See https://hexdocs.pm/elixir/Application.html
+ # for more information on OTP Applications
+ @moduledoc false
+
+ use Application
+
+ def start(_type, _args) do
+ children = [
+ # Start the Ecto repository
+ App.Repo,
+ # Start the Telemetry supervisor
+ AppWeb.Telemetry,
+ # Start the PubSub system
+ {Phoenix.PubSub, name: App.PubSub},
+ # Start the Endpoint (http/https)
+ AppWeb.Endpoint
+ # Start a worker by calling: App.Worker.start_link(arg)
+ # {App.Worker, arg}
+ ]
+
+ # See https://hexdocs.pm/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: App.Supervisor]
+ Supervisor.start_link(children, opts)
+ end
+
+ # Tell Phoenix to update the endpoint configuration
+ # whenever the application is updated.
+ def config_change(changed, _new, removed) do
+ AppWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+end
diff --git a/lib/app/repo.ex b/lib/app/repo.ex
new file mode 100644
index 00000000..857bd3f9
--- /dev/null
+++ b/lib/app/repo.ex
@@ -0,0 +1,5 @@
+defmodule App.Repo do
+ use Ecto.Repo,
+ otp_app: :app,
+ adapter: Ecto.Adapters.Postgres
+end
diff --git a/lib/app/todo.ex b/lib/app/todo.ex
new file mode 100644
index 00000000..0c410b1f
--- /dev/null
+++ b/lib/app/todo.ex
@@ -0,0 +1,104 @@
+defmodule App.Todo do
+ @moduledoc """
+ The Todo context.
+ """
+
+ import Ecto.Query, warn: false
+ alias App.Repo
+
+ alias App.Todo.Item
+
+ @doc """
+ Returns the list of items.
+
+ ## Examples
+
+ iex> list_items()
+ [%Item{}, ...]
+
+ """
+ def list_items do
+ Item |> order_by(asc: :id) |> Repo.all()
+ end
+
+ @doc """
+ Gets a single item.
+
+ Raises `Ecto.NoResultsError` if the Item does not exist.
+
+ ## Examples
+
+ iex> get_item!(123)
+ %Item{}
+
+ iex> get_item!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_item!(id), do: Repo.get!(Item, id)
+
+ @doc """
+ Creates a item.
+
+ ## Examples
+
+ iex> create_item(%{field: value})
+ {:ok, %Item{}}
+
+ iex> create_item(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_item(attrs \\ %{}) do
+ %Item{}
+ |> Item.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a item.
+
+ ## Examples
+
+ iex> update_item(item, %{field: new_value})
+ {:ok, %Item{}}
+
+ iex> update_item(item, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_item(%Item{} = item, attrs) do
+ item
+ |> Item.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a item.
+
+ ## Examples
+
+ iex> delete_item(item)
+ {:ok, %Item{}}
+
+ iex> delete_item(item)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_item(%Item{} = item) do
+ Repo.delete(item)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking item changes.
+
+ ## Examples
+
+ iex> change_item(item)
+ %Ecto.Changeset{data: %Item{}}
+
+ """
+ def change_item(%Item{} = item, attrs \\ %{}) do
+ Item.changeset(item, attrs)
+ end
+end
diff --git a/lib/app/todo/item.ex b/lib/app/todo/item.ex
new file mode 100644
index 00000000..af917734
--- /dev/null
+++ b/lib/app/todo/item.ex
@@ -0,0 +1,19 @@
+defmodule App.Todo.Item do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "items" do
+ field :person_id, :integer, default: 0
+ field :status, :integer, default: 0
+ field :text, :string
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(item, attrs) do
+ item
+ |> cast(attrs, [:text, :person_id, :status])
+ |> validate_required([:text])
+ end
+end
diff --git a/lib/app_web.ex b/lib/app_web.ex
new file mode 100644
index 00000000..bceb6b0b
--- /dev/null
+++ b/lib/app_web.ex
@@ -0,0 +1,80 @@
+defmodule AppWeb do
+ @moduledoc """
+ The entrypoint for defining your web interface, such
+ as controllers, views, channels and so on.
+
+ This can be used in your application as:
+
+ use AppWeb, :controller
+ use AppWeb, :view
+
+ The definitions below will be executed for every view,
+ controller, etc, so keep them short and clean, focused
+ on imports, uses and aliases.
+
+ Do NOT define functions inside the quoted expressions
+ below. Instead, define any helper function in modules
+ and import those modules here.
+ """
+
+ def controller do
+ quote do
+ use Phoenix.Controller, namespace: AppWeb
+
+ import Plug.Conn
+ import AppWeb.Gettext
+ alias AppWeb.Router.Helpers, as: Routes
+ end
+ end
+
+ def view do
+ quote do
+ use Phoenix.View,
+ root: "lib/app_web/templates",
+ namespace: AppWeb
+
+ # Import convenience functions from controllers
+ import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]
+
+ # Include shared imports and aliases for views
+ unquote(view_helpers())
+ end
+ end
+
+ def router do
+ quote do
+ use Phoenix.Router
+
+ import Plug.Conn
+ import Phoenix.Controller
+ end
+ end
+
+ def channel do
+ quote do
+ use Phoenix.Channel
+ import AppWeb.Gettext
+ end
+ end
+
+ defp view_helpers do
+ quote do
+ # Use all HTML functionality (forms, tags, etc)
+ use Phoenix.HTML
+
+ # Import basic rendering functionality (render, render_layout, etc)
+ import Phoenix.View
+
+ import AppWeb.ErrorHelpers
+ import AppWeb.Gettext
+ alias AppWeb.Router.Helpers, as: Routes
+ end
+ end
+
+ @doc """
+ When used, dispatch to the appropriate controller/view/etc.
+ """
+ defmacro __using__(which) when is_atom(which) do
+ apply(__MODULE__, which, [])
+ end
+end
diff --git a/web/channels/user_socket.ex b/lib/app_web/channels/user_socket.ex
similarity index 68%
rename from web/channels/user_socket.ex
rename to lib/app_web/channels/user_socket.ex
index 661b2d45..e2bef5dd 100644
--- a/web/channels/user_socket.ex
+++ b/lib/app_web/channels/user_socket.ex
@@ -1,12 +1,8 @@
-defmodule Api.UserSocket do
+defmodule AppWeb.UserSocket do
use Phoenix.Socket
## Channels
- # channel "room:*", Api.RoomChannel
-
- ## Transports
- transport :websocket, Phoenix.Transports.WebSocket
- # transport :longpoll, Phoenix.Transports.LongPoll
+ # channel "room:*", AppWeb.RoomChannel
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
@@ -19,19 +15,21 @@ defmodule Api.UserSocket do
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
- def connect(_params, socket) do
+ @impl true
+ def connect(_params, socket, _connect_info) do
{:ok, socket}
end
# Socket id's are topics that allow you to identify all sockets for a given user:
#
- # def id(socket), do: "users_socket:#{socket.assigns.user_id}"
+ # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
#
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
- # Api.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})
+ # AppWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
+ @impl true
def id(_socket), do: nil
end
diff --git a/lib/app_web/controllers/item_controller.ex b/lib/app_web/controllers/item_controller.ex
new file mode 100644
index 00000000..3dbe40d9
--- /dev/null
+++ b/lib/app_web/controllers/item_controller.ex
@@ -0,0 +1,95 @@
+defmodule AppWeb.ItemController do
+ use AppWeb, :controller
+
+ alias App.Todo
+ alias App.Todo.Item
+
+ def index(conn, params) do
+ item =
+ if not is_nil(params) and Map.has_key?(params, "id") do
+ Todo.get_item!(params["id"])
+ else
+ %Item{}
+ end
+
+ items = Todo.list_items()
+ changeset = Todo.change_item(item)
+
+ render(conn, "index.html",
+ items: items,
+ changeset: changeset,
+ editing: item,
+ filter: Map.get(params, "filter", "all")
+ )
+ end
+
+ def new(conn, _params) do
+ changeset = Todo.change_item(%Item{})
+ render(conn, "new.html", changeset: changeset)
+ end
+
+ def create(conn, %{"item" => item_params}) do
+ case Todo.create_item(item_params) do
+ {:ok, _item} ->
+ conn
+ |> put_flash(:info, "Item created successfully.")
+ |> redirect(to: Routes.item_path(conn, :index))
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ render(conn, "new.html", changeset: changeset)
+ end
+ end
+
+ def show(conn, %{"id" => id}) do
+ item = Todo.get_item!(id)
+ render(conn, "show.html", item: item)
+ end
+
+ def edit(conn, params) do
+ index(conn, params)
+ end
+
+ def update(conn, %{"id" => id, "item" => item_params}) do
+ item = Todo.get_item!(id)
+
+ case Todo.update_item(item, item_params) do
+ {:ok, _item} ->
+ conn
+ # |> put_flash(:info, "Item updated successfully.")
+ |> redirect(to: Routes.item_path(conn, :index))
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ render(conn, "edit.html", item: item, changeset: changeset)
+ end
+ end
+
+ def delete(conn, %{"id" => id}) do
+ item = Todo.get_item!(id)
+ Todo.update_item(item, %{status: 2})
+ index(conn, %{})
+ end
+
+ def toggle_status(item) do
+ case item.status do
+ 1 -> 0
+ 0 -> 1
+ end
+ end
+
+ def toggle(conn, %{"id" => id}) do
+ item = Todo.get_item!(id)
+ Todo.update_item(item, %{status: toggle_status(item)})
+ redirect(conn, to: Routes.item_path(conn, :index))
+ end
+
+ import Ecto.Query
+ alias App.Repo
+
+ def clear_completed(conn, _param) do
+ person_id = 0
+ query = from(i in Item, where: i.person_id == ^person_id, where: i.status == 1)
+ Repo.update_all(query, set: [status: 2])
+ # render the main template:
+ index(conn, %{filter: "all"})
+ end
+end
diff --git a/lib/api/endpoint.ex b/lib/app_web/endpoint.ex
similarity index 52%
rename from lib/api/endpoint.ex
rename to lib/app_web/endpoint.ex
index 12465bf2..661564c3 100644
--- a/lib/api/endpoint.ex
+++ b/lib/app_web/endpoint.ex
@@ -1,14 +1,29 @@
-defmodule Api.Endpoint do
- use Phoenix.Endpoint, otp_app: :api
+defmodule AppWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :app
- socket "/socket", Api.UserSocket
+ # The session will be stored in the cookie and signed,
+ # this means its contents can be read but not tampered with.
+ # Set :encryption_salt if you would also like to encrypt it.
+ @session_options [
+ store: :cookie,
+ key: "_app_key",
+ signing_salt: "EaSsAcnS"
+ ]
+
+ socket "/socket", AppWeb.UserSocket,
+ websocket: true,
+ longpoll: false
+
+ socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
# Serve at "/" the static files from "priv/static" directory.
#
- # You should set gzip to true if you are running phoenix.digest
+ # You should set gzip to true if you are running phx.digest
# when deploying your static files in production.
plug Plug.Static,
- at: "/", from: :api, gzip: false,
+ at: "/",
+ from: :app,
+ gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
# Code reloading can be explicitly enabled under the
@@ -17,26 +32,23 @@ defmodule Api.Endpoint do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
+ plug Phoenix.Ecto.CheckRepoStatus, otp_app: :app
end
+ plug Phoenix.LiveDashboard.RequestLogger,
+ param_key: "request_logger",
+ cookie_key: "request_logger"
+
plug Plug.RequestId
- plug Plug.Logger
+ plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
- json_decoder: Poison
+ json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
-
- # The session will be stored in the cookie and signed,
- # this means its contents can be read but not tampered with.
- # Set :encryption_salt if you would also like to encrypt it.
- plug Plug.Session,
- store: :cookie,
- key: "_api_key",
- signing_salt: "BjlUvgQk"
-
- plug Api.Router
+ plug Plug.Session, @session_options
+ plug AppWeb.Router
end
diff --git a/web/gettext.ex b/lib/app_web/gettext.ex
similarity index 61%
rename from web/gettext.ex
rename to lib/app_web/gettext.ex
index e378f805..19bc04ee 100644
--- a/web/gettext.ex
+++ b/lib/app_web/gettext.ex
@@ -1,24 +1,24 @@
-defmodule Api.Gettext do
+defmodule AppWeb.Gettext do
@moduledoc """
A module providing Internationalization with a gettext-based API.
By using [Gettext](https://hexdocs.pm/gettext),
your module gains a set of macros for translations, for example:
- import Api.Gettext
+ import AppWeb.Gettext
# Simple translation
- gettext "Here is the string to translate"
+ gettext("Here is the string to translate")
# Plural translation
- ngettext "Here is the string to translate",
+ ngettext("Here is the string to translate",
"Here are the strings to translate",
- 3
+ 3)
# Domain-based translation
- dgettext "errors", "Here is the error message to translate"
+ dgettext("errors", "Here is the error message to translate")
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
"""
- use Gettext, otp_app: :api
+ use Gettext, otp_app: :app
end
diff --git a/lib/app_web/router.ex b/lib/app_web/router.ex
new file mode 100644
index 00000000..6983e82a
--- /dev/null
+++ b/lib/app_web/router.ex
@@ -0,0 +1,46 @@
+defmodule AppWeb.Router do
+ use AppWeb, :router
+
+ pipeline :browser do
+ plug :accepts, ["html"]
+ plug :fetch_session
+ plug :fetch_flash
+ plug :protect_from_forgery
+ plug :put_secure_browser_headers
+ end
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/", AppWeb do
+ pipe_through :browser
+
+ get "/", ItemController, :index
+ resources "/items", ItemController
+ get "/items/toggle/:id", ItemController, :toggle
+ get "/clear", ItemController, :clear_completed
+ get "/:filter", ItemController, :index
+ end
+
+ # Other scopes may use custom stacks.
+ # scope "/api", AppWeb do
+ # pipe_through :api
+ # end
+
+ # Enables LiveDashboard only for development
+ #
+ # If you want to use the LiveDashboard in production, you should put
+ # it behind authentication and allow only admins to access it.
+ # If your application does not have an admins-only section yet,
+ # you can use Plug.BasicAuth to set up some basic authentication
+ # as long as you are also using SSL (which you should anyway).
+ if Mix.env() in [:dev, :test] do
+ import Phoenix.LiveDashboard.Router
+
+ scope "/" do
+ pipe_through :browser
+ live_dashboard "/dashboard", metrics: AppWeb.Telemetry
+ end
+ end
+end
diff --git a/lib/app_web/telemetry.ex b/lib/app_web/telemetry.ex
new file mode 100644
index 00000000..20839ca4
--- /dev/null
+++ b/lib/app_web/telemetry.ex
@@ -0,0 +1,53 @@
+defmodule AppWeb.Telemetry do
+ use Supervisor
+ import Telemetry.Metrics
+
+ def start_link(arg) do
+ Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
+ end
+
+ @impl true
+ def init(_arg) do
+ children = [
+ {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
+ # Add reporters as children of your supervision tree.
+ # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
+ ]
+
+ Supervisor.init(children, strategy: :one_for_one)
+ end
+
+ def metrics do
+ [
+ # Phoenix Metrics
+ summary("phoenix.endpoint.stop.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.stop.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+
+ # Database Metrics
+ summary("app.repo.query.total_time", unit: {:native, :millisecond}),
+ summary("app.repo.query.decode_time", unit: {:native, :millisecond}),
+ summary("app.repo.query.query_time", unit: {:native, :millisecond}),
+ summary("app.repo.query.queue_time", unit: {:native, :millisecond}),
+ summary("app.repo.query.idle_time", unit: {:native, :millisecond}),
+
+ # VM Metrics
+ summary("vm.memory.total", unit: {:byte, :kilobyte}),
+ summary("vm.total_run_queue_lengths.total"),
+ summary("vm.total_run_queue_lengths.cpu"),
+ summary("vm.total_run_queue_lengths.io")
+ ]
+ end
+
+ defp periodic_measurements do
+ [
+ # A module, function and arguments to be invoked periodically.
+ # This function must call :telemetry.execute/3 and a metric must be added above.
+ # {AppWeb, :count_users, []}
+ ]
+ end
+end
diff --git a/lib/app_web/templates/item/edit.html.eex b/lib/app_web/templates/item/edit.html.eex
new file mode 100644
index 00000000..8f5718ed
--- /dev/null
+++ b/lib/app_web/templates/item/edit.html.eex
@@ -0,0 +1,6 @@
+
Edit Item
+
+<%= render "form.html",
+ Map.put(assigns, :action, Routes.item_path(@conn, :update, @item)) %>
+
+<%= link "Back", to: Routes.item_path(@conn, :index) %>
diff --git a/lib/app_web/templates/item/form.html.eex b/lib/app_web/templates/item/form.html.eex
new file mode 100644
index 00000000..21e081bd
--- /dev/null
+++ b/lib/app_web/templates/item/form.html.eex
@@ -0,0 +1,6 @@
+<%= form_for @changeset, @action, fn f -> %>
+ <%= text_input f, :text, placeholder: "what needs to be done?",
+ class: "new-todo", autofocus: "" %>
+
<%= submit "Save" %>
+
+<% end %>
diff --git a/lib/app_web/templates/item/index.html.eex b/lib/app_web/templates/item/index.html.eex
new file mode 100644
index 00000000..c11752b4
--- /dev/null
+++ b/lib/app_web/templates/item/index.html.eex
@@ -0,0 +1,75 @@
+
+
+
todos
+ <%= if @editing.id do %>
+
+ Click here to create a new item!
+
+ <% else %>
+ <%= render "form.html", Map.put(assigns, :action, Routes.item_path(@conn, :create)) %>
+ <% end %>
+
+
+
+
+
+
+ <%= for item <- filter(@items, @filter) do %>
+
diff --git a/web/views/error_helpers.ex b/web/views/error_helpers.ex
deleted file mode 100644
index 107e98fe..00000000
--- a/web/views/error_helpers.ex
+++ /dev/null
@@ -1,40 +0,0 @@
-defmodule Api.ErrorHelpers do
- @moduledoc """
- Conveniences for translating and building error messages.
- """
-
- use Phoenix.HTML
-
- @doc """
- Generates tag for inlined form input errors.
- """
- def error_tag(form, field) do
- if error = form.errors[field] do
- content_tag :span, translate_error(error), class: "help-block"
- end
- end
-
- @doc """
- Translates an error message using gettext.
- """
- def translate_error({msg, opts}) do
- # Because error messages were defined within Ecto, we must
- # call the Gettext module passing our Gettext backend. We
- # also use the "errors" domain as translations are placed
- # in the errors.po file.
- # Ecto will pass the :count keyword if the error message is
- # meant to be pluralized.
- # On your own code and templates, depending on whether you
- # need the message to be pluralized or not, this could be
- # written simply as:
- #
- # dngettext "errors", "1 file", "%{count} files", count
- # dgettext "errors", "is invalid"
- #
- if count = opts[:count] do
- Gettext.dngettext(Api.Gettext, "errors", msg, msg, count, opts)
- else
- Gettext.dgettext(Api.Gettext, "errors", msg, opts)
- end
- end
-end
diff --git a/web/views/error_view.ex b/web/views/error_view.ex
deleted file mode 100644
index b1725e39..00000000
--- a/web/views/error_view.ex
+++ /dev/null
@@ -1,17 +0,0 @@
-defmodule Api.ErrorView do
- use Api.Web, :view
-
- def render("404.html", _assigns) do
- "Page not found"
- end
-
- def render("500.html", _assigns) do
- "Internal server error"
- end
-
- # In case no render clause matches or no
- # template is found, let's render it as 500
- def template_not_found(_template, assigns) do
- render "500.html", assigns
- end
-end
diff --git a/web/views/layout_view.ex b/web/views/layout_view.ex
deleted file mode 100644
index 0e0f4944..00000000
--- a/web/views/layout_view.ex
+++ /dev/null
@@ -1,3 +0,0 @@
-defmodule Api.LayoutView do
- use Api.Web, :view
-end
diff --git a/web/views/page_view.ex b/web/views/page_view.ex
deleted file mode 100644
index 69e35a99..00000000
--- a/web/views/page_view.ex
+++ /dev/null
@@ -1,3 +0,0 @@
-defmodule Api.PageView do
- use Api.Web, :view
-end
diff --git a/web/web.ex b/web/web.ex
deleted file mode 100644
index 8f1c3b68..00000000
--- a/web/web.ex
+++ /dev/null
@@ -1,81 +0,0 @@
-defmodule Api.Web do
- @moduledoc """
- A module that keeps using definitions for controllers,
- views and so on.
-
- This can be used in your application as:
-
- use Api.Web, :controller
- use Api.Web, :view
-
- The definitions below will be executed for every view,
- controller, etc, so keep them short and clean, focused
- on imports, uses and aliases.
-
- Do NOT define functions inside the quoted expressions
- below.
- """
-
- def model do
- quote do
- use Ecto.Schema
-
- import Ecto
- import Ecto.Changeset
- import Ecto.Query
- end
- end
-
- def controller do
- quote do
- use Phoenix.Controller
-
- alias Api.Repo
- import Ecto
- import Ecto.Query
-
- import Api.Router.Helpers
- import Api.Gettext
- end
- end
-
- def view do
- quote do
- use Phoenix.View, root: "web/templates"
-
- # Import convenience functions from controllers
- import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
-
- # Use all HTML functionality (forms, tags, etc)
- use Phoenix.HTML
-
- import Api.Router.Helpers
- import Api.ErrorHelpers
- import Api.Gettext
- end
- end
-
- def router do
- quote do
- use Phoenix.Router
- end
- end
-
- def channel do
- quote do
- use Phoenix.Channel
-
- alias Api.Repo
- import Ecto
- import Ecto.Query
- import Api.Gettext
- end
- end
-
- @doc """
- When used, dispatch to the appropriate controller/view/etc.
- """
- defmacro __using__(which) when is_atom(which) do
- apply(__MODULE__, which, [])
- end
-end