├── lib
├── todo_test_web
│ ├── templates
│ │ ├── layout
│ │ │ ├── app.html.eex
│ │ │ └── app.html.eex.back
│ │ └── page
│ │ │ └── index.html.eex
│ ├── views
│ │ ├── page_view.ex
│ │ ├── layout_view.ex
│ │ ├── error_view.ex
│ │ ├── todo_view.ex
│ │ ├── changeset_view.ex
│ │ └── error_helpers.ex
│ ├── controllers
│ │ ├── page_controller.ex
│ │ ├── fallback_controller.ex
│ │ └── todo_controller.ex
│ ├── router.ex
│ ├── gettext.ex
│ ├── channels
│ │ ├── user_socket.ex
│ │ └── todo_channel.ex
│ └── endpoint.ex
├── todo_test.ex
├── todo_test
│ ├── repo.ex
│ ├── todos
│ │ ├── todo.ex
│ │ └── todos.ex
│ └── application.ex
└── todo_test_web.ex
├── test
├── test_helper.exs
├── todo_test_web
│ ├── views
│ │ ├── page_view_test.exs
│ │ ├── layout_view_test.exs
│ │ └── error_view_test.exs
│ ├── controllers
│ │ ├── page_controller_test.exs
│ │ └── todo_controller_test.exs
│ └── channels
│ │ └── todo_channel_test.exs
├── support
│ ├── channel_case.ex
│ ├── conn_case.ex
│ └── data_case.ex
└── todo_test
│ └── todos
│ └── todos_test.exs
├── priv
├── static
│ ├── favicon.ico
│ ├── static
│ │ └── css
│ │ │ ├── main.65027555.css
│ │ │ └── main.65027555.css.map
│ ├── asset-manifest.json
│ ├── robots.txt
│ ├── manifest.json
│ ├── index.html
│ └── service-worker.js
├── repo
│ ├── migrations
│ │ ├── 20171026170000_add_todo_completed_field.exs
│ │ └── 20171026014808_create_todo.exs
│ └── seeds.exs
└── gettext
│ ├── en
│ └── LC_MESSAGES
│ │ └── errors.po
│ └── errors.pot
├── assets
├── public
│ ├── favicon.ico
│ ├── robots.txt
│ ├── manifest.json
│ └── index.html
├── src
│ ├── App.test.js
│ ├── actions
│ │ ├── channel.js
│ │ └── index.js
│ ├── index.css
│ ├── components
│ │ ├── Todo.jsx
│ │ ├── TodoList.jsx
│ │ ├── AddTodo.jsx
│ │ └── Footer.jsx
│ ├── index.js
│ ├── reducers
│ │ └── index.js
│ ├── containers
│ │ └── App.jsx
│ ├── logo.svg
│ ├── registerServiceWorker.js
│ └── vendor
│ │ └── phoenix.js
├── .gitignore
├── config
│ ├── jest
│ │ ├── fileTransform.js
│ │ └── cssTransform.js
│ ├── polyfills.js
│ ├── paths.js
│ ├── env.js
│ ├── webpackDevServer.config.js
│ ├── webpack.config.dev.js
│ └── webpack.config.prod.js
├── scripts
│ ├── test.js
│ ├── start.js
│ └── build.js
└── package.json
├── .gitignore
├── config
├── test.exs
├── config.exs
├── dev.exs
└── prod.exs
├── README.md
├── mix.exs
├── rel
└── config.exs
└── mix.lock
/lib/todo_test_web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 | ../../../../priv/static/index.html
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
3 | Ecto.Adapters.SQL.Sandbox.mode(TodoTest.Repo, :manual)
4 |
5 |
--------------------------------------------------------------------------------
/lib/todo_test_web/views/page_view.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.PageView do
2 | use TodoTestWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/lib/todo_test_web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.LayoutView do
2 | use TodoTestWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/priv/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blaze33/elixir-phoenix-react-redux-todo-list/HEAD/priv/static/favicon.ico
--------------------------------------------------------------------------------
/assets/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blaze33/elixir-phoenix-react-redux-todo-list/HEAD/assets/public/favicon.ico
--------------------------------------------------------------------------------
/priv/static/static/css/main.65027555.css:
--------------------------------------------------------------------------------
1 | body{margin:0;padding:0;font-family:sans-serif}
2 | /*# sourceMappingURL=main.65027555.css.map*/
--------------------------------------------------------------------------------
/test/todo_test_web/views/page_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.PageViewTest do
2 | use TodoTestWeb.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/test/todo_test_web/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.LayoutViewTest do
2 | use TodoTestWeb.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/lib/todo_test_web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.PageController do
2 | use TodoTestWeb, :controller
3 |
4 | def index(conn, _params) do
5 | render conn, "index.html"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/assets/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/priv/static/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "main.css": "static/css/main.65027555.css",
3 | "main.css.map": "static/css/main.65027555.css.map",
4 | "main.js": "static/js/main.065d4dbc.js",
5 | "main.js.map": "static/js/main.065d4dbc.js.map"
6 | }
--------------------------------------------------------------------------------
/priv/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/assets/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render( , div);
8 | });
9 |
--------------------------------------------------------------------------------
/assets/src/actions/channel.js:
--------------------------------------------------------------------------------
1 | import { Socket } from 'phoenix'
2 |
3 | export function configureChannel() {
4 | const socket = new Socket('ws://127.0.0.1:4000/socket');
5 | socket.connect();
6 | const channel = socket.channel('todo');
7 | return channel
8 | }
--------------------------------------------------------------------------------
/priv/repo/migrations/20171026170000_add_todo_completed_field.exs:
--------------------------------------------------------------------------------
1 | defmodule TodoTest.Repo.Migrations.AddTodoCompletedField do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:todo) do
6 | add :completed, :boolean
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/todo_test_web/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.PageControllerTest do
2 | use TodoTestWeb.ConnCase
3 |
4 | test "GET /", %{conn: conn} do
5 | conn = get conn, "/"
6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20171026014808_create_todo.exs:
--------------------------------------------------------------------------------
1 | defmodule TodoTest.Repo.Migrations.CreateTodo do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:todo) do
6 | add :label, :string
7 |
8 | timestamps()
9 | end
10 |
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/todo_test.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTest do
2 | @moduledoc """
3 | TodoTest keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 | end
10 |
--------------------------------------------------------------------------------
/priv/static/static/css/main.65027555.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["index.css"],"names":[],"mappings":"AAAA,KACE,SACA,UACA,sBAAwB","file":"static/css/main.65027555.css","sourcesContent":["body {\n margin: 0;\n padding: 0;\n font-family: sans-serif;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/index.css"],"sourceRoot":""}
--------------------------------------------------------------------------------
/assets/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Ubuntu');
2 |
3 | body {
4 | text-align: center;
5 | font-family: 'Ubuntu';
6 | padding: 2em;
7 | }
8 |
9 | ul {
10 | position: relative;
11 | left: 0;
12 | right: 0;
13 | margin: 1em auto;
14 | max-width: 50vw;
15 | }
--------------------------------------------------------------------------------
/lib/todo_test/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTest.Repo do
2 | use Ecto.Repo, otp_app: :todo_test
3 |
4 | @doc """
5 | Dynamically loads the repository url from the
6 | DATABASE_URL environment variable.
7 | """
8 | def init(_, opts) do
9 | {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/assets/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/priv/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/assets/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/assets/config/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 |
5 | // This is a custom Jest transformer turning file imports into filenames.
6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html
7 |
8 | module.exports = {
9 | process(src, filename) {
10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`;
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/assets/config/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This is a custom Jest transformer turning style imports into empty objects.
4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html
5 |
6 | module.exports = {
7 | process() {
8 | return 'module.exports = {};';
9 | },
10 | getCacheKey() {
11 | // The output is always the same.
12 | return 'cssTransform';
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | # Script for populating the database. You can run it as:
2 | #
3 | # mix run priv/repo/seeds.exs
4 | #
5 | # Inside the script, you can read and write to any of your
6 | # repositories directly:
7 | #
8 | # TodoTest.Repo.insert!(%TodoTest.SomeSchema{})
9 | #
10 | # We recommend using the bang functions (`insert!`, `update!`
11 | # and so on) as they will fail if something goes wrong.
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # App artifacts
2 | /_build
3 | /db
4 | /deps
5 | /*.ez
6 |
7 | # Generated on crash by the VM
8 | erl_crash.dump
9 |
10 | # Files matching config/*.secret.exs pattern contain sensitive
11 | # data and you should not commit them into version control.
12 | #
13 | # Alternatively, you may comment the line below and commit the
14 | # secrets files as long as you replace their contents by environment
15 | # variables.
16 | /config/*.secret.exs
17 |
--------------------------------------------------------------------------------
/lib/todo_test/todos/todo.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTest.Todos.Todo do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 | alias TodoTest.Todos.Todo
5 |
6 |
7 | schema "todo" do
8 | field :label, :string
9 | field :completed, :boolean
10 |
11 | timestamps()
12 | end
13 |
14 | @doc false
15 | def changeset(%Todo{} = todo, attrs) do
16 | todo
17 | |> cast(attrs, [:label, :completed])
18 | |> validate_required([:label, :completed])
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/todo_test_web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.ErrorView do
2 | use TodoTestWeb, :view
3 |
4 | def render("404.html", _assigns) do
5 | "Page not found"
6 | end
7 |
8 | def render("500.html", _assigns) do
9 | "Internal server error"
10 | end
11 |
12 | # In case no render clause matches or no
13 | # template is found, let's render it as 500
14 | def template_not_found(_template, assigns) do
15 | render "500.html", assigns
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/todo_test_web/views/todo_view.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.TodoView do
2 | use TodoTestWeb, :view
3 | alias TodoTestWeb.TodoView
4 |
5 | def render("index.json", %{todo: todo}) do
6 | %{data: render_many(todo, TodoView, "todo.json")}
7 | end
8 |
9 | def render("show.json", %{todo: todo}) do
10 | %{data: render_one(todo, TodoView, "todo.json")}
11 | end
12 |
13 | def render("todo.json", %{todo: todo}) do
14 | %{id: todo.id,
15 | label: todo.label,
16 | completed: todo.completed}
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/priv/static/index.html:
--------------------------------------------------------------------------------
1 |
React App You need to enable JavaScript to run this app.
--------------------------------------------------------------------------------
/assets/src/components/Todo.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class Todo extends Component {
4 | render() {
5 | return (
6 |
12 | {this.props.label}
13 | {e.stopPropagation(); this.props.onClickDelete()}} style={{float: 'right'}}>✘
14 |
15 | )
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :todo_test, TodoTestWeb.Endpoint,
6 | http: [port: 4001],
7 | server: false
8 |
9 | # Print only warnings and errors during test
10 | config :logger, level: :warn
11 |
12 | # Configure your database
13 | config :todo_test, TodoTest.Repo,
14 | adapter: Ecto.Adapters.Postgres,
15 | username: "postgres",
16 | password: "postgres",
17 | database: "todo_test_test",
18 | hostname: "localhost",
19 | pool: Ecto.Adapters.SQL.Sandbox
20 |
--------------------------------------------------------------------------------
/assets/src/components/TodoList.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Todo from './Todo';
3 |
4 | export default class TodoList extends Component {
5 | render() {
6 | if (this.props.isLoading) {
7 | return (
8 | Loading ...
9 | );
10 | }
11 |
12 | return (
13 |
14 | {this.props.todos.map((todo, index) =>
15 | this.props.onTodoClickDelete(todo)}
18 | onClick={() => this.props.onTodoClick(todo)} />
19 | )}
20 |
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/todo_test_web/views/changeset_view.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.ChangesetView do
2 | use TodoTestWeb, :view
3 |
4 | @doc """
5 | Traverses and translates changeset errors.
6 |
7 | See `Ecto.Changeset.traverse_errors/2` and
8 | `TodoTestWeb.ErrorHelpers.translate_error/1` for more details.
9 | """
10 | def translate_errors(changeset) do
11 | Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
12 | end
13 |
14 | def render("error.json", %{changeset: changeset}) do
15 | # When encoded, the changeset returns its errors
16 | # as a JSON object. So we just pass it forward.
17 | %{errors: translate_errors(changeset)}
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/todo_test_web/controllers/fallback_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.FallbackController do
2 | @moduledoc """
3 | Translates controller action results into valid `Plug.Conn` responses.
4 |
5 | See `Phoenix.Controller.action_fallback/1` for more details.
6 | """
7 | use TodoTestWeb, :controller
8 |
9 | def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
10 | conn
11 | |> put_status(:unprocessable_entity)
12 | |> render(TodoTestWeb.ChangesetView, "error.json", changeset: changeset)
13 | end
14 |
15 | def call(conn, {:error, :not_found}) do
16 | conn
17 | |> put_status(:not_found)
18 | |> render(TodoTestWeb.ErrorView, :"404")
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/assets/config/polyfills.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | if (typeof Promise === 'undefined') {
4 | // Rejection tracking prevents a common issue where React gets into an
5 | // inconsistent state due to an error, but it gets swallowed by a Promise,
6 | // and the user has no idea what causes React's erratic future behavior.
7 | require('promise/lib/rejection-tracking').enable();
8 | window.Promise = require('promise/lib/es6-extensions.js');
9 | }
10 |
11 | // fetch() polyfill for making API calls.
12 | require('whatwg-fetch');
13 |
14 | // Object.assign() is commonly used with React.
15 | // It will use the native implementation if it's present and isn't buggy.
16 | Object.assign = require('object-assign');
17 |
--------------------------------------------------------------------------------
/test/todo_test_web/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.ErrorViewTest do
2 | use TodoTestWeb.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(TodoTestWeb.ErrorView, "404.html", []) ==
9 | "Page not found"
10 | end
11 |
12 | test "render 500.html" do
13 | assert render_to_string(TodoTestWeb.ErrorView, "500.html", []) ==
14 | "Internal server error"
15 | end
16 |
17 | test "render any other" do
18 | assert render_to_string(TodoTestWeb.ErrorView, "505.html", []) ==
19 | "Internal server error"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/assets/src/components/AddTodo.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class AddTodo extends Component {
4 | render() {
5 | return (
6 |
7 | this.handleKeyPress(e)} />
8 | this.handleClick(e)}>
9 | Add
10 |
11 |
12 | )
13 | }
14 |
15 | handleClick(e) {
16 | const node = this.refs.input
17 | const text = node.value.trim()
18 | if (!text) return
19 | this.props.onAddClick(text)
20 | node.value = ''
21 | }
22 |
23 | handleKeyPress(e) {
24 | if (e.charCode === 13) {
25 | this.handleClick(e);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/todo_test_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.Router do
2 | use TodoTestWeb, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_flash
8 | plug :protect_from_forgery
9 | plug :put_secure_browser_headers
10 | end
11 |
12 | pipeline :api do
13 | plug :accepts, ["json"]
14 | end
15 |
16 | scope "/", TodoTestWeb do
17 | pipe_through :browser # Use the default browser stack
18 |
19 | get "/", PageController, :index
20 | end
21 |
22 | # Other scopes may use custom stacks.
23 | # scope "/api", TodoTestWeb do
24 | # pipe_through :api
25 | # end
26 | resources "/todo", TodoTestWeb.TodoController, except: [:new, :edit]
27 | end
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TodoTest
2 |
3 | To start your Phoenix server:
4 |
5 | * Install dependencies with `mix deps.get`
6 | * Create and migrate your database with `mix ecto.create && mix ecto.migrate`
7 | * Start Phoenix endpoint with `mix phx.server`
8 |
9 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
10 |
11 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment).
12 |
13 | ## Learn more
14 |
15 | * Official website: http://www.phoenixframework.org/
16 | * Guides: http://phoenixframework.org/docs/overview
17 | * Docs: https://hexdocs.pm/phoenix
18 | * Mailing list: http://groups.google.com/group/phoenix-talk
19 | * Source: https://github.com/phoenixframework/phoenix
20 |
--------------------------------------------------------------------------------
/assets/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class Footer extends Component {
4 | renderFilter(filter, name) {
5 | if (filter === this.props.filter) {
6 | return name
7 | }
8 |
9 | return (
10 | {
11 | e.preventDefault()
12 | this.props.onFilterChange(filter)
13 | }}>
14 | {name}
15 |
16 | )
17 | }
18 |
19 | render() {
20 | return (
21 |
22 | Show:
23 | {' '}
24 | {this.renderFilter('SHOW_ALL', 'All')}
25 | {', '}
26 | {this.renderFilter('SHOW_COMPLETED', 'Completed')}
27 | {', '}
28 | {this.renderFilter('SHOW_ACTIVE', 'Active')}
29 | .
30 |
31 | )
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/todo_test_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import TodoTestWeb.Gettext
9 |
10 | # Simple translation
11 | gettext "Here is the string to translate"
12 |
13 | # Plural translation
14 | ngettext "Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3
17 |
18 | # Domain-based translation
19 | dgettext "errors", "Here is the error message to translate"
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :todo_test
24 | end
25 |
--------------------------------------------------------------------------------
/assets/scripts/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Do this as the first thing so that any code reading it knows the right env.
4 | process.env.BABEL_ENV = 'test';
5 | process.env.NODE_ENV = 'test';
6 | process.env.PUBLIC_URL = '';
7 |
8 | // Makes the script crash on unhandled rejections instead of silently
9 | // ignoring them. In the future, promise rejections that are not handled will
10 | // terminate the Node.js process with a non-zero exit code.
11 | process.on('unhandledRejection', err => {
12 | throw err;
13 | });
14 |
15 | // Ensure environment variables are read.
16 | require('../config/env');
17 |
18 | const jest = require('jest');
19 | const argv = process.argv.slice(2);
20 |
21 | // Watch unless on CI or in coverage mode
22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) {
23 | argv.push('--watch');
24 | }
25 |
26 |
27 | jest.run(argv);
28 |
--------------------------------------------------------------------------------
/assets/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import thunkMiddleware from 'redux-thunk';
4 | import { createStore, applyMiddleware } from 'redux';
5 | import { Provider } from 'react-redux';
6 | import {createLogger} from 'redux-logger';
7 | import registerServiceWorker from './registerServiceWorker';
8 | import App from './containers/App';
9 | import todoApp from './reducers';
10 |
11 | import 'normalize.css'
12 | import './index.css';
13 |
14 | const loggerMiddleware = createLogger();
15 |
16 | const createStoreWithMiddleware = applyMiddleware(
17 | thunkMiddleware, // lets us dispatch() functions
18 | loggerMiddleware
19 | )(createStore);
20 |
21 | const store = createStoreWithMiddleware(todoApp);
22 |
23 | render(
24 |
25 |
26 | ,
27 | document.getElementById('root')
28 | );
29 | registerServiceWorker();
30 |
--------------------------------------------------------------------------------
/test/todo_test_web/channels/todo_channel_test.exs:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.TodoChannelTest do
2 | use TodoTestWeb.ChannelCase
3 |
4 | alias TodoTestWeb.TodoChannel
5 |
6 | setup do
7 | {:ok, _, socket} =
8 | socket("user_id", %{some: :assign})
9 | |> subscribe_and_join(TodoChannel, "todo:lobby")
10 |
11 | {:ok, socket: socket}
12 | end
13 |
14 | test "ping replies with status ok", %{socket: socket} do
15 | ref = push socket, "ping", %{"hello" => "there"}
16 | assert_reply ref, :ok, %{"hello" => "there"}
17 | end
18 |
19 | test "shout broadcasts to todo:lobby", %{socket: socket} do
20 | push socket, "shout", %{"hello" => "all"}
21 | assert_broadcast "shout", %{"hello" => "all"}
22 | end
23 |
24 | test "broadcasts are pushed to the client", %{socket: socket} do
25 | broadcast_from! socket, "broadcast", %{"some" => "data"}
26 | assert_push "broadcast", %{"some" => "data"}
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 | use Mix.Config
7 |
8 | # General application configuration
9 | config :todo_test,
10 | ecto_repos: [TodoTest.Repo]
11 |
12 | # Configures the endpoint
13 | config :todo_test, TodoTestWeb.Endpoint,
14 | url: [host: "localhost"],
15 | secret_key_base: "NmwMWEO6KltgwPtsiUvU7wHjD1CA5hvN7FxLurSx0JFSzLTnJAyKNeyvxfZMfg7m",
16 | render_errors: [view: TodoTestWeb.ErrorView, accepts: ~w(html json)],
17 | pubsub: [name: TodoTest.PubSub,
18 | adapter: Phoenix.PubSub.PG2]
19 |
20 | # Configures Elixir's Logger
21 | config :logger, :console,
22 | format: "$time $metadata[$level] $message\n",
23 | metadata: [:request_id]
24 |
25 | # Import environment specific config. This must remain at the bottom
26 | # of this file so it overrides the configuration defined above.
27 | import_config "#{Mix.env}.exs"
28 |
--------------------------------------------------------------------------------
/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | import other functionality to make it easier
8 | to build common datastructures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with channels
21 | use Phoenix.ChannelTest
22 |
23 | # The default endpoint for testing
24 | @endpoint TodoTestWeb.Endpoint
25 | end
26 | end
27 |
28 |
29 | setup tags do
30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(TodoTest.Repo)
31 | unless tags[:async] do
32 | Ecto.Adapters.SQL.Sandbox.mode(TodoTest.Repo, {:shared, self()})
33 | end
34 | :ok
35 | end
36 |
37 | end
38 |
--------------------------------------------------------------------------------
/lib/todo_test_web/templates/page/index.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
<%= gettext "Welcome to %{name}!", name: "Phoenix" %>
3 |
A productive web framework that does not compromise speed and maintainability.
4 |
5 |
6 |
37 |
--------------------------------------------------------------------------------
/lib/todo_test/application.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTest.Application do
2 | use Application
3 |
4 | # See https://hexdocs.pm/elixir/Application.html
5 | # for more information on OTP Applications
6 | def start(_type, _args) do
7 | import Supervisor.Spec
8 |
9 | # Define workers and child supervisors to be supervised
10 | children = [
11 | # Start the Ecto repository
12 | supervisor(TodoTest.Repo, []),
13 | # Start the endpoint when the application starts
14 | supervisor(TodoTestWeb.Endpoint, []),
15 | # Start your own worker by calling: TodoTest.Worker.start_link(arg1, arg2, arg3)
16 | # worker(TodoTest.Worker, [arg1, arg2, arg3]),
17 | ]
18 |
19 | # See https://hexdocs.pm/elixir/Supervisor.html
20 | # for other strategies and supported options
21 | opts = [strategy: :one_for_one, name: TodoTest.Supervisor]
22 | Supervisor.start_link(children, opts)
23 | end
24 |
25 | # Tell Phoenix to update the endpoint configuration
26 | # whenever the application is updated.
27 | def config_change(changed, _new, removed) do
28 | TodoTestWeb.Endpoint.config_change(changed, removed)
29 | :ok
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | import other functionality to make it easier
8 | to build common datastructures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with connections
21 | use Phoenix.ConnTest
22 | import TodoTestWeb.Router.Helpers
23 |
24 | # The default endpoint for testing
25 | @endpoint TodoTestWeb.Endpoint
26 | end
27 | end
28 |
29 |
30 | setup tags do
31 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(TodoTest.Repo)
32 | unless tags[:async] do
33 | Ecto.Adapters.SQL.Sandbox.mode(TodoTest.Repo, {:shared, self()})
34 | end
35 | {:ok, conn: Phoenix.ConnTest.build_conn()}
36 | end
37 |
38 | end
39 |
--------------------------------------------------------------------------------
/lib/todo_test_web/templates/layout/app.html.eex.back:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Hello TodoTest!
11 | ">
12 |
13 |
14 |
15 |
16 |
24 |
25 |
<%= get_flash(@conn, :info) %>
26 |
<%= get_flash(@conn, :error) %>
27 |
28 |
29 | <%= render @view_module, @view_template, assigns %>
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/lib/todo_test_web/controllers/todo_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.TodoController do
2 | use TodoTestWeb, :controller
3 |
4 | alias TodoTest.Todos
5 | alias TodoTest.Todos.Todo
6 |
7 | action_fallback TodoTestWeb.FallbackController
8 |
9 | def index(conn, _params) do
10 | todo = Todos.list_todo()
11 | render(conn, "index.json", todo: todo)
12 | end
13 |
14 | def create(conn, %{"todo" => todo_params}) do
15 | with {:ok, %Todo{} = todo} <- Todos.create_todo(todo_params) do
16 | conn
17 | |> put_status(:created)
18 | |> put_resp_header("location", todo_path(conn, :show, todo))
19 | |> render("show.json", todo: todo)
20 | end
21 | end
22 |
23 | def show(conn, %{"id" => id}) do
24 | todo = Todos.get_todo!(id)
25 | render(conn, "show.json", todo: todo)
26 | end
27 |
28 | def update(conn, %{"id" => id, "todo" => todo_params}) do
29 | todo = Todos.get_todo!(id)
30 |
31 | with {:ok, %Todo{} = todo} <- Todos.update_todo(todo, todo_params) do
32 | render(conn, "show.json", todo: todo)
33 | end
34 | end
35 |
36 | def delete(conn, %{"id" => id}) do
37 | todo = Todos.get_todo!(id)
38 | with {:ok, %Todo{}} <- Todos.delete_todo(todo) do
39 | send_resp(conn, :no_content, "")
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/todo_test_web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.UserSocket do
2 | use Phoenix.Socket
3 |
4 | ## Channels
5 | # channel "room:*", TodoTestWeb.RoomChannel
6 | channel "todo", TodoTestWeb.TodoChannel
7 |
8 | ## Transports
9 | transport :websocket, Phoenix.Transports.WebSocket
10 | # transport :longpoll, Phoenix.Transports.LongPoll
11 |
12 | # Socket params are passed from the client and can
13 | # be used to verify and authenticate a user. After
14 | # verification, you can put default assigns into
15 | # the socket that will be set for all channels, ie
16 | #
17 | # {:ok, assign(socket, :user_id, verified_user_id)}
18 | #
19 | # To deny connection, return `:error`.
20 | #
21 | # See `Phoenix.Token` documentation for examples in
22 | # performing token verification on connect.
23 | def connect(_params, socket) do
24 | {:ok, socket}
25 | end
26 |
27 | # Socket id's are topics that allow you to identify all sockets for a given user:
28 | #
29 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
30 | #
31 | # Would allow you to broadcast a "disconnect" event and terminate
32 | # all active sockets and channels for a given user:
33 | #
34 | # TodoTestWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
35 | #
36 | # Returning `nil` makes this socket anonymous.
37 | def id(_socket), do: nil
38 | end
39 |
--------------------------------------------------------------------------------
/lib/todo_test_web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | Enum.map(Keyword.get_values(form.errors, field), fn (error) ->
13 | content_tag :span, translate_error(error), class: "help-block"
14 | end)
15 | end
16 |
17 | @doc """
18 | Translates an error message using gettext.
19 | """
20 | def translate_error({msg, opts}) do
21 | # Because error messages were defined within Ecto, we must
22 | # call the Gettext module passing our Gettext backend. We
23 | # also use the "errors" domain as translations are placed
24 | # in the errors.po file.
25 | # Ecto will pass the :count keyword if the error message is
26 | # meant to be pluralized.
27 | # On your own code and templates, depending on whether you
28 | # need the message to be pluralized or not, this could be
29 | # written simply as:
30 | #
31 | # dngettext "errors", "1 file", "%{count} files", count
32 | # dgettext "errors", "is invalid"
33 | #
34 | if count = opts[:count] do
35 | Gettext.dngettext(TodoTestWeb.Gettext, "errors", msg, msg, count, opts)
36 | else
37 | Gettext.dgettext(TodoTestWeb.Gettext, "errors", msg, opts)
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/test/support/data_case.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTest.DataCase do
2 | @moduledoc """
3 | This module defines the setup for tests requiring
4 | access to the application's data layer.
5 |
6 | You may define functions here to be used as helpers in
7 | your tests.
8 |
9 | Finally, if the test case interacts with the database,
10 | it cannot be async. For this reason, every test runs
11 | inside a transaction which is reset at the beginning
12 | of the test unless the test case is marked as async.
13 | """
14 |
15 | use ExUnit.CaseTemplate
16 |
17 | using do
18 | quote do
19 | alias TodoTest.Repo
20 |
21 | import Ecto
22 | import Ecto.Changeset
23 | import Ecto.Query
24 | import TodoTest.DataCase
25 | end
26 | end
27 |
28 | setup tags do
29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(TodoTest.Repo)
30 |
31 | unless tags[:async] do
32 | Ecto.Adapters.SQL.Sandbox.mode(TodoTest.Repo, {:shared, self()})
33 | end
34 |
35 | :ok
36 | end
37 |
38 | @doc """
39 | A helper that transform changeset errors to a map of messages.
40 |
41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"})
42 | assert "password is too short" in errors_on(changeset).password
43 | assert %{password: ["password is too short"]} = errors_on(changeset)
44 |
45 | """
46 | def errors_on(changeset) do
47 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
48 | Enum.reduce(opts, message, fn {key, value}, acc ->
49 | String.replace(acc, "%{#{key}}", to_string(value))
50 | end)
51 | end)
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/assets/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/lib/todo_test_web/channels/todo_channel.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.TodoChannel do
2 | use TodoTestWeb, :channel
3 |
4 | def join("todo", payload, socket) do
5 | if authorized?(payload) do
6 | todos = TodoTest.Todos.list_todo()
7 | {:ok, %{todos: TodoTestWeb.TodoView.render("index.json", todo: todos)}, socket}
8 | else
9 | {:error, %{reason: "unauthorized"}}
10 | end
11 | end
12 |
13 | # Channels can be used in a request/response fashion
14 | # by sending replies to requests from the client
15 | def handle_in("ping", payload, socket) do
16 | {:reply, {:ok, payload}, socket}
17 | end
18 |
19 | # It is also common to receive messages from the client and
20 | # broadcast to everyone in the current topic (todo:lobby).
21 | def handle_in("shout", payload, socket) do
22 | broadcast socket, "shout", payload
23 | {:noreply, socket}
24 | end
25 |
26 | def handle_in("new:todo", payload, socket) do
27 | {:ok, created} = TodoTest.Todos.create_todo(payload)
28 | created_json = TodoTestWeb.TodoView.render("todo.json", todo: created)
29 | broadcast! socket, "new:todo", created_json
30 | {:reply, {:ok, created_json}, socket}
31 | end
32 |
33 | def handle_in("update:todo", payload, socket) do
34 | todo = TodoTest.Todos.get_todo!(payload["id"])
35 | TodoTest.Todos.update_todo(todo, payload)
36 | broadcast! socket, "update:todo", payload
37 | {:noreply, socket}
38 | end
39 |
40 | def handle_in("delete:todo", payload, socket) do
41 | todo = TodoTest.Todos.get_todo!(payload["id"])
42 | TodoTest.Todos.delete_todo(todo)
43 | broadcast! socket, "delete:todo", payload
44 | {:noreply, socket}
45 | end
46 |
47 | # Add authorization logic here as required.
48 | defp authorized?(_payload) do
49 | true
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule TodoTest.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :todo_test,
7 | version: "0.0.1",
8 | elixir: "~> 1.4",
9 | elixirc_paths: elixirc_paths(Mix.env),
10 | compilers: [:phoenix, :gettext] ++ Mix.compilers,
11 | start_permanent: Mix.env == :prod,
12 | aliases: aliases(),
13 | deps: deps()
14 | ]
15 | end
16 |
17 | # Configuration for the OTP application.
18 | #
19 | # Type `mix help compile.app` for more information.
20 | def application do
21 | [
22 | mod: {TodoTest.Application, []},
23 | extra_applications: [:logger, :runtime_tools]
24 | ]
25 | end
26 |
27 | # Specifies which paths to compile per environment.
28 | defp elixirc_paths(:test), do: ["lib", "test/support"]
29 | defp elixirc_paths(_), do: ["lib"]
30 |
31 | # Specifies your project dependencies.
32 | #
33 | # Type `mix help deps` for examples and options.
34 | defp deps do
35 | [
36 | {:phoenix, "~> 1.3.0"},
37 | {:phoenix_pubsub, "~> 1.0"},
38 | {:phoenix_ecto, "~> 3.2"},
39 | {:postgrex, ">= 0.0.0"},
40 | {:phoenix_html, "~> 2.10"},
41 | {:phoenix_live_reload, "~> 1.0", only: :dev},
42 | {:gettext, "~> 0.11"},
43 | {:cowboy, "~> 1.0"},
44 | {:distillery, "~> 1.5", runtime: false}
45 | ]
46 | end
47 |
48 | # Aliases are shortcuts or tasks specific to the current project.
49 | # For example, to create, migrate and run the seeds file at once:
50 | #
51 | # $ mix ecto.setup
52 | #
53 | # See the documentation for `Mix` for more info on aliases.
54 | defp aliases do
55 | [
56 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
57 | "ecto.reset": ["ecto.drop", "ecto.setup"],
58 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]
59 | ]
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/todo_test_web.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb do
2 | @moduledoc """
3 | The entrypoint for defining your web interface, such
4 | as controllers, views, channels and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use TodoTestWeb, :controller
9 | use TodoTestWeb, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below. Instead, define any helper function in modules
17 | and import those modules here.
18 | """
19 |
20 | def controller do
21 | quote do
22 | use Phoenix.Controller, namespace: TodoTestWeb
23 | import Plug.Conn
24 | import TodoTestWeb.Router.Helpers
25 | import TodoTestWeb.Gettext
26 | end
27 | end
28 |
29 | def view do
30 | quote do
31 | use Phoenix.View, root: "lib/todo_test_web/templates",
32 | namespace: TodoTestWeb
33 |
34 | # Import convenience functions from controllers
35 | import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
36 |
37 | # Use all HTML functionality (forms, tags, etc)
38 | use Phoenix.HTML
39 |
40 | import TodoTestWeb.Router.Helpers
41 | import TodoTestWeb.ErrorHelpers
42 | import TodoTestWeb.Gettext
43 | end
44 | end
45 |
46 | def router do
47 | quote do
48 | use Phoenix.Router
49 | import Plug.Conn
50 | import Phoenix.Controller
51 | end
52 | end
53 |
54 | def channel do
55 | quote do
56 | use Phoenix.Channel
57 | import TodoTestWeb.Gettext
58 | end
59 | end
60 |
61 | @doc """
62 | When used, dispatch to the appropriate controller/view/etc.
63 | """
64 | defmacro __using__(which) when is_atom(which) do
65 | apply(__MODULE__, which, [])
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/todo_test_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule TodoTestWeb.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :todo_test
3 |
4 | socket "/socket", TodoTestWeb.UserSocket
5 |
6 | # Serve at "/" the static files from "priv/static" directory.
7 | #
8 | # You should set gzip to true if you are running phoenix.digest
9 | # when deploying your static files in production.
10 | plug Plug.Static,
11 | at: "/", from: :todo_test, gzip: false,
12 | only: ~w(css fonts images js favicon.ico robots.txt static)
13 |
14 | # Code reloading can be explicitly enabled under the
15 | # :code_reloader configuration of your endpoint.
16 | if code_reloading? do
17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
18 | plug Phoenix.LiveReloader
19 | plug Phoenix.CodeReloader
20 | end
21 |
22 | plug Plug.RequestId
23 | plug Plug.Logger
24 |
25 | plug Plug.Parsers,
26 | parsers: [:urlencoded, :multipart, :json],
27 | pass: ["*/*"],
28 | json_decoder: Poison
29 |
30 | plug Plug.MethodOverride
31 | plug Plug.Head
32 |
33 | # The session will be stored in the cookie and signed,
34 | # this means its contents can be read but not tampered with.
35 | # Set :encryption_salt if you would also like to encrypt it.
36 | plug Plug.Session,
37 | store: :cookie,
38 | key: "_todo_test_key",
39 | signing_salt: "0i/ABWG8"
40 |
41 | plug TodoTestWeb.Router
42 |
43 | @doc """
44 | Callback invoked for dynamically configuring the endpoint.
45 |
46 | It receives the endpoint configuration and checks if
47 | configuration should be loaded from the system environment.
48 | """
49 | def init(_key, config) do
50 | if config[:load_from_system_env] do
51 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
52 | {:ok, Keyword.put(config, :http, [:inet6, port: port])}
53 | else
54 | {:ok, config}
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/rel/config.exs:
--------------------------------------------------------------------------------
1 | # Import all plugins from `rel/plugins`
2 | # They can then be used by adding `plugin MyPlugin` to
3 | # either an environment, or release definition, where
4 | # `MyPlugin` is the name of the plugin module.
5 | Path.join(["rel", "plugins", "*.exs"])
6 | |> Path.wildcard()
7 | |> Enum.map(&Code.eval_file(&1))
8 |
9 | use Mix.Releases.Config,
10 | # This sets the default release built by `mix release`
11 | default_release: :default,
12 | # This sets the default environment used by `mix release`
13 | default_environment: Mix.env()
14 |
15 | # For a full list of config options for both releases
16 | # and environments, visit https://hexdocs.pm/distillery/configuration.html
17 |
18 |
19 | # You may define one or more environments in this file,
20 | # an environment's settings will override those of a release
21 | # when building in that environment, this combination of release
22 | # and environment configuration is called a profile
23 |
24 | environment :dev do
25 | # If you are running Phoenix, you should make sure that
26 | # server: true is set and the code reloader is disabled,
27 | # even in dev mode.
28 | # It is recommended that you build with MIX_ENV=prod and pass
29 | # the --env flag to Distillery explicitly if you want to use
30 | # dev mode.
31 | set dev_mode: true
32 | set include_erts: false
33 | set cookie: :"lgPzVTWD{3_JB)D7m_Nlu!e~{t(1^_~t]kGC,[kO?fknjI*jqDgYSDgJY$[^`Jt<"
34 | end
35 |
36 | environment :prod do
37 | set include_erts: true
38 | set include_src: false
39 | set cookie: :"A(v*A&As138=eu$>9DCDSPHTeKxXPD*[!(t6`&ndX[t^/x~P2a;}_?b0794~@I%0"
40 | end
41 |
42 | # You may define one or more releases in this file.
43 | # If you have not set a default release, or selected one
44 | # when running `mix release`, the first release in the file
45 | # will be used by default
46 |
47 | release :todo_test do
48 | set version: current_version(:todo_test)
49 | set applications: [
50 | :runtime_tools
51 | ]
52 | end
53 |
54 |
--------------------------------------------------------------------------------
/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For development, we disable any cache and enable
4 | # debugging and code reloading.
5 | #
6 | # The watchers configuration can be used to run external
7 | # watchers to your application. For example, we use it
8 | # with brunch.io to recompile .js and .css sources.
9 | config :todo_test, TodoTestWeb.Endpoint,
10 | http: [port: 4000],
11 | debug_errors: true,
12 | code_reloader: true,
13 | check_origin: false,
14 | watchers: []
15 |
16 | # ## SSL Support
17 | #
18 | # In order to use HTTPS in development, a self-signed
19 | # certificate can be generated by running the following
20 | # command from your terminal:
21 | #
22 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem
23 | #
24 | # The `http:` config above can be replaced with:
25 | #
26 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"],
27 | #
28 | # If desired, both `http:` and `https:` keys can be
29 | # configured to run both http and https servers on
30 | # different ports.
31 |
32 | # Watch static and templates for browser reloading.
33 | config :todo_test, TodoTestWeb.Endpoint,
34 | live_reload: [
35 | patterns: [
36 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
37 | ~r{priv/gettext/.*(po)$},
38 | ~r{lib/todo_test_web/views/.*(ex)$},
39 | ~r{lib/todo_test_web/templates/.*(eex)$}
40 | ]
41 | ],
42 | url: [host: "127.0.0.1"]
43 |
44 | # Do not include metadata nor timestamps in development logs
45 | config :logger, :console, format: "[$level] $message\n"
46 |
47 | # Set a higher stacktrace during development. Avoid configuring such
48 | # in production as building large stacktraces may be expensive.
49 | config :phoenix, :stacktrace_depth, 20
50 |
51 | # Configure your database
52 | config :todo_test, TodoTest.Repo,
53 | adapter: Ecto.Adapters.Postgres,
54 | username: "dev",
55 | password: "password",
56 | database: "todo_test_dev",
57 | hostname: "localhost",
58 | pool_size: 10
59 |
--------------------------------------------------------------------------------
/test/todo_test/todos/todos_test.exs:
--------------------------------------------------------------------------------
1 | defmodule TodoTest.TodosTest do
2 | use TodoTest.DataCase
3 |
4 | alias TodoTest.Todos
5 |
6 | describe "todo" do
7 | alias TodoTest.Todos.Todo
8 |
9 | @valid_attrs %{label: "some label"}
10 | @update_attrs %{label: "some updated label"}
11 | @invalid_attrs %{label: nil}
12 |
13 | def todo_fixture(attrs \\ %{}) do
14 | {:ok, todo} =
15 | attrs
16 | |> Enum.into(@valid_attrs)
17 | |> Todos.create_todo()
18 |
19 | todo
20 | end
21 |
22 | test "list_todo/0 returns all todo" do
23 | todo = todo_fixture()
24 | assert Todos.list_todo() == [todo]
25 | end
26 |
27 | test "get_todo!/1 returns the todo with given id" do
28 | todo = todo_fixture()
29 | assert Todos.get_todo!(todo.id) == todo
30 | end
31 |
32 | test "create_todo/1 with valid data creates a todo" do
33 | assert {:ok, %Todo{} = todo} = Todos.create_todo(@valid_attrs)
34 | assert todo.label == "some label"
35 | end
36 |
37 | test "create_todo/1 with invalid data returns error changeset" do
38 | assert {:error, %Ecto.Changeset{}} = Todos.create_todo(@invalid_attrs)
39 | end
40 |
41 | test "update_todo/2 with valid data updates the todo" do
42 | todo = todo_fixture()
43 | assert {:ok, todo} = Todos.update_todo(todo, @update_attrs)
44 | assert %Todo{} = todo
45 | assert todo.label == "some updated label"
46 | end
47 |
48 | test "update_todo/2 with invalid data returns error changeset" do
49 | todo = todo_fixture()
50 | assert {:error, %Ecto.Changeset{}} = Todos.update_todo(todo, @invalid_attrs)
51 | assert todo == Todos.get_todo!(todo.id)
52 | end
53 |
54 | test "delete_todo/1 deletes the todo" do
55 | todo = todo_fixture()
56 | assert {:ok, %Todo{}} = Todos.delete_todo(todo)
57 | assert_raise Ecto.NoResultsError, fn -> Todos.get_todo!(todo.id) end
58 | end
59 |
60 | test "change_todo/1 returns a todo changeset" do
61 | todo = todo_fixture()
62 | assert %Ecto.Changeset{} = Todos.change_todo(todo)
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/assets/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import {
3 | UPDATE_TODO_REQUEST, UPDATE_TODO_SUCCESS, UPDATE_TODO_FAILURE,
4 | FETCH_TODOS_REQUEST, FETCH_TODOS_SUCCESS, FETCH_TODOS_FAILURE,
5 | ADD_TODO_REQUEST, ADD_TODO_SUCCESS, ADD_TODO_FAILURE,
6 | COMPLETE_TODO, DELETE_TODO_SUCCESS,
7 | SET_VISIBILITY_FILTER, VisibilityFilters } from '../actions';
8 | const { SHOW_ALL } = VisibilityFilters;
9 |
10 | function visibilityFilter(state = SHOW_ALL, action) {
11 | switch (action.type) {
12 | case SET_VISIBILITY_FILTER:
13 | return action.filter;
14 |
15 | default:
16 | return state;
17 | }
18 | }
19 |
20 | function todos(state = [], action) {
21 | switch (action.type) {
22 | case FETCH_TODOS_SUCCESS:
23 | return [].concat(action.todos);
24 |
25 | case ADD_TODO_REQUEST:
26 | return state;
27 |
28 | case ADD_TODO_SUCCESS:
29 | console.log(action)
30 | return [
31 | ...state,
32 | action.todo
33 | ];
34 |
35 | case ADD_TODO_FAILURE:
36 | console.error('ADD_TODO_FAILURE');
37 | return state;
38 |
39 | case UPDATE_TODO_REQUEST:
40 | return state;
41 |
42 | case UPDATE_TODO_SUCCESS:
43 | return state.map(todo => todo.id === action.todo.id ? action.todo : todo)
44 |
45 | case UPDATE_TODO_FAILURE:
46 | console.error('UPDATE_TODO_FAILURE');
47 | return state;
48 |
49 | case COMPLETE_TODO:
50 | return state.map(todo => todo.id === action.id ? {...todo, completed: true} : todo)
51 |
52 | case DELETE_TODO_SUCCESS:
53 | return state.filter(todo => todo.id !== action.todo.id)
54 |
55 | default:
56 | return state;
57 | }
58 | }
59 |
60 | function isLoading(state = false, action) {
61 | switch (action.type) {
62 | case FETCH_TODOS_REQUEST:
63 | return true;
64 |
65 | case FETCH_TODOS_SUCCESS:
66 | case FETCH_TODOS_FAILURE:
67 | return false;
68 |
69 | default:
70 | return state;
71 | }
72 | }
73 |
74 | const todoApp = combineReducers({
75 | visibilityFilter,
76 | todos,
77 | isLoading
78 | });
79 |
80 | export default todoApp;
--------------------------------------------------------------------------------
/assets/src/containers/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { deleteTodo, fetchTodos, addTodo, updateTodo, setVisibilityFilter, VisibilityFilters } from '../actions';
4 | import AddTodo from '../components/AddTodo';
5 | import TodoList from '../components/TodoList';
6 | import Footer from '../components/Footer';
7 |
8 | class App extends Component {
9 | componentDidMount() {
10 | let { dispatch } = this.props;
11 |
12 | dispatch(fetchTodos());
13 | }
14 |
15 | render() {
16 | // Injected by connect() call:
17 | const { dispatch, visibleTodos, isLoading, visibilityFilter } = this.props;
18 |
19 | return (
20 |
21 |
23 | dispatch(addTodo(text))
24 | } />
25 |
29 | dispatch(updateTodo(todo))
30 | }
31 | onTodoClickDelete={todo =>
32 | dispatch(deleteTodo(todo))
33 | } />
34 |
37 | dispatch(setVisibilityFilter(nextFilter))
38 | } />
39 |
40 | );
41 | }
42 | }
43 |
44 |
45 | function selectTodos(todos, filter) {
46 | switch (filter) {
47 | case VisibilityFilters.SHOW_ALL:
48 | return todos;
49 | case VisibilityFilters.SHOW_COMPLETED:
50 | return todos.filter(todo => todo.completed);
51 | case VisibilityFilters.SHOW_ACTIVE:
52 | return todos.filter(todo => !todo.completed);
53 | }
54 | }
55 |
56 | // Which props do we want to inject, given the global state?
57 | // Note: use https://github.com/faassen/reselect for better performance.
58 | function select(state) {
59 | return {
60 | visibleTodos: selectTodos(state.todos, state.visibilityFilter),
61 | visibilityFilter: state.visibilityFilter,
62 | isLoading: state.isLoading
63 | };
64 | }
65 |
66 | // Wrap the component to inject dispatch and state into it
67 | export default connect(select)(App);
--------------------------------------------------------------------------------
/assets/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const url = require('url');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebookincubator/create-react-app/issues/637
9 | const appDirectory = fs.realpathSync(process.cwd());
10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11 |
12 | const envPublicUrl = process.env.PUBLIC_URL;
13 |
14 | function ensureSlash(path, needsSlash) {
15 | const hasSlash = path.endsWith('/');
16 | if (hasSlash && !needsSlash) {
17 | return path.substr(path, path.length - 1);
18 | } else if (!hasSlash && needsSlash) {
19 | return `${path}/`;
20 | } else {
21 | return path;
22 | }
23 | }
24 |
25 | const getPublicUrl = appPackageJson =>
26 | envPublicUrl || require(appPackageJson).homepage;
27 |
28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
29 | // "public path" at which the app is served.
30 | // Webpack needs to know it to put the right