├── .gitignore ├── LICENSE ├── README.md ├── brunch-config.js ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── lib ├── hello_phoenix.ex └── hello_phoenix │ ├── endpoint.ex │ ├── guardian_serializer.ex │ └── repo.ex ├── mix.exs ├── mix.lock ├── package.json ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot └── repo │ ├── migrations │ └── 20160129095239_create_user.exs │ └── seeds.exs ├── test ├── controllers │ └── page_controller_test.exs ├── models │ └── user_test.exs ├── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── model_case.ex ├── test_helper.exs └── views │ ├── error_view_test.exs │ ├── layout_view_test.exs │ └── page_view_test.exs └── web ├── channels ├── user_channel.ex └── user_socket.ex ├── controllers ├── api │ └── v1 │ │ ├── current_user_controller.ex │ │ ├── registration_controller.ex │ │ └── session_controller.ex └── page_controller.ex ├── gettext.ex ├── helpers └── session.ex ├── models └── user.ex ├── router.ex ├── static ├── assets │ ├── favicon.ico │ ├── images │ │ └── phoenix.png │ └── robots.txt ├── css-burrito-config.json ├── css │ ├── app.sass │ ├── bitters │ │ ├── _base.sass │ │ ├── _buttons.sass │ │ ├── _forms.sass │ │ ├── _grid-settings.sass │ │ ├── _lists.sass │ │ ├── _tables.sass │ │ ├── _typography.sass │ │ └── _variables.sass │ ├── global │ │ ├── _base.sass │ │ ├── _layout.sass │ │ ├── _settings.sass │ │ ├── _skin.sass │ │ ├── _typography.sass │ │ └── _utilities.sass │ ├── libs │ │ ├── _library-variable-overrides.sass │ │ └── _normalize.sass │ └── modules │ │ ├── _example-module.sass │ │ └── _modules.sass └── js │ ├── actions │ ├── header.js │ ├── registrations.js │ └── sessions.js │ ├── app.js │ ├── constants │ └── index.js │ ├── containers │ ├── authenticated.js │ └── root.js │ ├── layouts │ ├── header.js │ └── main.js │ ├── reducers │ ├── header.js │ ├── index.js │ ├── registration.js │ └── session.js │ ├── routes │ └── index.js │ ├── store │ └── index.js │ ├── utils │ └── index.js │ └── views │ ├── home │ └── index.js │ ├── registrations │ └── new.js │ └── sessions │ └── new.js ├── templates ├── layout │ └── app.html.eex └── page │ └── index.html.eex ├── views ├── current_user_view.ex ├── error_helpers.ex ├── error_view.ex ├── layout_view.ex ├── page_view.ex ├── registration_view.ex └── session_view.ex └── web.ex /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generate on crash by the VM 8 | erl_crash.dump 9 | 10 | # Static artifacts 11 | /node_modules 12 | 13 | # Since we are building assets from web/static, 14 | # we ignore priv/static. You may want to comment 15 | # this depending on your deployment strategy. 16 | /priv/static/ 17 | 18 | # The config/prod.secret.exs file by default contains sensitive 19 | # data and you should not commit it into version control. 20 | # 21 | # Alternatively, you may comment the line below and commit the 22 | # secrets file as long as you replace its contents by environment 23 | # variables. 24 | /config/prod.secret.exs 25 | /priv/static/css/app.css 26 | /priv/static/js/app.js 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ricardo García Vega 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phoenix template 2 | 3 | This is a template for new Phoenix single page applications with: 4 | 5 | - **React** 6 | - **Redux** 7 | - **React router** 8 | - **ES6/7** 9 | - **Sass** with **Bourbon**, **Neat** and **Bitters** 10 | - Authentication via **Guardian** and **jwt** 11 | 12 | **User** base migration and both **registration** and **authentication** controllers already implemented. 13 | 14 | To start your Phoenix app: 15 | 16 | * Install dependencies with `mix deps.get` 17 | * Create and migrate your database with `mix ecto.create && mix ecto.migrate` 18 | * Install Node.js dependencies with `npm install` 19 | * Start Phoenix endpoint with `mix phoenix.server` 20 | 21 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 22 | 23 | Happy coding! 24 | 25 | ## License 26 | 27 | See [LICENSE](LICENSE). 28 | -------------------------------------------------------------------------------- /brunch-config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // See http://brunch.io/#documentation for docs. 3 | files: { 4 | javascripts: { 5 | joinTo: 'js/app.js', 6 | 7 | // To use a separate vendor.js bundle, specify two files path 8 | // https://github.com/brunch/brunch/blob/stable/docs/config.md#files 9 | // joinTo: { 10 | // "js/app.js": /^(web\/static\/js)/, 11 | // "js/vendor.js": /^(web\/static\/vendor)|(deps)/ 12 | // } 13 | // 14 | // To change the order of concatenation of files, explicitly mention here 15 | // https://github.com/brunch/brunch/tree/master/docs#concatenation 16 | // order: { 17 | // before: [ 18 | // "web/static/vendor/js/jquery-2.1.1.js", 19 | // "web/static/vendor/js/bootstrap.min.js" 20 | // ] 21 | // } 22 | }, 23 | stylesheets: { 24 | joinTo: 'css/app.css', 25 | }, 26 | templates: { 27 | joinTo: 'js/app.js', 28 | }, 29 | }, 30 | 31 | conventions: { 32 | // This option sets where we should place non-css and non-js assets in. 33 | // By default, we set this to "/web/static/assets". Files in this directory 34 | // will be copied to `paths.public`, which is "priv/static" by default. 35 | assets: /^(web\/static\/assets)/, 36 | }, 37 | 38 | // Phoenix paths configuration 39 | paths: { 40 | // Dependencies and current project directories to watch 41 | watched: [ 42 | 'web/static', 43 | 'test/static', 44 | ], 45 | 46 | // Where to compile files to 47 | public: 'priv/static', 48 | }, 49 | 50 | // Configure your plugins 51 | plugins: { 52 | babel: { 53 | presets: ['es2015', 'react', 'stage-2', 'stage-0'], 54 | 55 | // Do not use ES6 compiler in vendor code 56 | ignore: [/web\/static\/vendor/], 57 | }, 58 | sass: { 59 | options: { 60 | includePaths: ['node_modules'], 61 | }, 62 | }, 63 | }, 64 | 65 | modules: { 66 | autoRequire: { 67 | 'js/app.js': ['web/static/js/app'], 68 | }, 69 | }, 70 | 71 | npm: { 72 | enabled: true, 73 | 74 | // Whitelist the npm deps to be pulled in as front-end assets. 75 | // All other deps in package.json will be excluded from the bundle. 76 | whitelist: [ 77 | 'bourbon', 78 | 'bourbon-neat', 79 | 'classnames', 80 | 'es6-promise', 81 | 'history', 82 | 'invariant', 83 | 'isomorphic-fetch', 84 | 'moment', 85 | 'phoenix', 86 | 'phoenix_html', 87 | 'react', 88 | 'react-addons-css-transition-group', 89 | 'react-dom', 90 | 'react-gravatar', 91 | 'react-redux', 92 | 'react-router', 93 | 'react-router-redux', 94 | 'redux', 95 | 'redux-logger', 96 | 'redux-thunk', 97 | ], 98 | }, 99 | }; 100 | -------------------------------------------------------------------------------- /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 | # Configures the endpoint 9 | config :hello_phoenix, HelloPhoenix.Endpoint, 10 | url: [host: "localhost"], 11 | root: Path.dirname(__DIR__), 12 | secret_key_base: "fwhgWA/EEgRzxIZuGabA7q+HWvDqgQvQqTHGK99X4H6nL0vUl19Hjv2/+VgWGUhQ", 13 | render_errors: [accepts: ~w(html json)], 14 | pubsub: [name: HelloPhoenix.PubSub, 15 | adapter: Phoenix.PubSub.PG2] 16 | 17 | # Configures Elixir's Logger 18 | config :logger, :console, 19 | format: "$time $metadata[$level] $message\n", 20 | metadata: [:request_id] 21 | 22 | # Import environment specific config. This must remain at the bottom 23 | # of this file so it overrides the configuration defined above. 24 | import_config "#{Mix.env}.exs" 25 | 26 | # Configure phoenix generators 27 | config :phoenix, :generators, 28 | migration: true, 29 | binary_id: false 30 | 31 | # Configure guardian 32 | config :guardian, Guardian, 33 | issuer: "HelloPhoenix", 34 | ttl: { 3, :days }, 35 | verify_issuer: true, 36 | serializer: HelloPhoenix.GuardianSerializer 37 | -------------------------------------------------------------------------------- /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 :hello_phoenix, HelloPhoenix.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin"]] 15 | 16 | # Watch static and templates for browser reloading. 17 | config :hello_phoenix, HelloPhoenix.Endpoint, 18 | live_reload: [ 19 | patterns: [ 20 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 21 | ~r{priv/gettext/.*(po)$}, 22 | ~r{web/views/.*(ex)$}, 23 | ~r{web/templates/.*(eex)$} 24 | ] 25 | ] 26 | 27 | # Do not include metadata nor timestamps in development logs 28 | config :logger, :console, format: "[$level] $message\n" 29 | 30 | # Set a higher stacktrace during development. 31 | # Do not configure such in production as keeping 32 | # and calculating stacktraces is usually expensive. 33 | config :phoenix, :stacktrace_depth, 20 34 | 35 | # Configure your database 36 | config :hello_phoenix, HelloPhoenix.Repo, 37 | adapter: Ecto.Adapters.Postgres, 38 | username: "postgres", 39 | password: "postgres", 40 | database: "hello_phoenix_dev", 41 | hostname: "localhost", 42 | pool_size: 10 43 | 44 | # Guardian configuration 45 | config :guardian, Guardian, 46 | secret_key: "Y8+P3Plvr/7bDo38ySz5s8K1hRpzERiDmmjw4v7W+7EQ2XAFG/qdZhE0xFE8Be8D" 47 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we configure the host to read the PORT 4 | # from the system environment. Therefore, you will need 5 | # to set PORT=80 before running your server. 6 | # 7 | # You should also configure the url host to something 8 | # meaningful, we use this information when generating URLs. 9 | # 10 | # Finally, we also include the path to a manifest 11 | # containing the digested version of static files. This 12 | # manifest is generated by the mix phoenix.digest task 13 | # which you typically run after static files are built. 14 | config :hello_phoenix, HelloPhoenix.Endpoint, 15 | http: [port: {:system, "PORT"}], 16 | url: [host: "example.com", port: 80], 17 | cache_static_manifest: "priv/static/manifest.json" 18 | 19 | # Do not print debug messages in production 20 | config :logger, level: :info 21 | 22 | # ## SSL Support 23 | # 24 | # To get SSL working, you will need to add the `https` key 25 | # to the previous section and set your `:url` port to 443: 26 | # 27 | # config :hello_phoenix, HelloPhoenix.Endpoint, 28 | # ... 29 | # url: [host: "example.com", port: 443], 30 | # https: [port: 443, 31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 33 | # 34 | # Where those two env variables return an absolute path to 35 | # the key and cert in disk or a relative path inside priv, 36 | # for example "priv/ssl/server.key". 37 | # 38 | # We also recommend setting `force_ssl`, ensuring no data is 39 | # ever sent via http, always redirecting to https: 40 | # 41 | # config :hello_phoenix, HelloPhoenix.Endpoint, 42 | # force_ssl: [hsts: true] 43 | # 44 | # Check `Plug.SSL` for all available options in `force_ssl`. 45 | 46 | # ## Using releases 47 | # 48 | # If you are doing OTP releases, you need to instruct Phoenix 49 | # to start the server for all endpoints: 50 | # 51 | # config :phoenix, :serve_endpoints, true 52 | # 53 | # Alternatively, you can configure exactly which server to 54 | # start per endpoint: 55 | # 56 | # config :hello_phoenix, HelloPhoenix.Endpoint, server: true 57 | # 58 | # You will also need to set the application root to `.` in order 59 | # for the new static assets to be served after a hot upgrade: 60 | # 61 | # config :hello_phoenix, HelloPhoenix.Endpoint, root: "." 62 | 63 | # Finally import the config/prod.secret.exs 64 | # which should be versioned separately. 65 | import_config "prod.secret.exs" 66 | -------------------------------------------------------------------------------- /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 :hello_phoenix, HelloPhoenix.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 :hello_phoenix, HelloPhoenix.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: "postgres", 16 | password: "postgres", 17 | database: "hello_phoenix_test", 18 | hostname: "localhost", 19 | pool: Ecto.Adapters.SQL.Sandbox 20 | -------------------------------------------------------------------------------- /lib/hello_phoenix.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | children = [ 10 | # Start the endpoint when the application starts 11 | supervisor(HelloPhoenix.Endpoint, []), 12 | # Start the Ecto repository 13 | supervisor(HelloPhoenix.Repo, []), 14 | # Here you could define other workers and supervisors as children 15 | # worker(HelloPhoenix.Worker, [arg1, arg2, arg3]), 16 | ] 17 | 18 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 19 | # for other strategies and supported options 20 | opts = [strategy: :one_for_one, name: HelloPhoenix.Supervisor] 21 | Supervisor.start_link(children, opts) 22 | end 23 | 24 | # Tell Phoenix to update the endpoint configuration 25 | # whenever the application is updated. 26 | def config_change(changed, _new, removed) do 27 | HelloPhoenix.Endpoint.config_change(changed, removed) 28 | :ok 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/hello_phoenix/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :hello_phoenix 3 | 4 | socket "/socket", HelloPhoenix.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: :hello_phoenix, gzip: false, 12 | only: ~w(css fonts images js favicon.ico robots.txt) 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 | plug Plug.Session, 34 | store: :cookie, 35 | key: "_hello_phoenix_key", 36 | signing_salt: "0NfPCK+m" 37 | 38 | plug HelloPhoenix.Router 39 | end 40 | -------------------------------------------------------------------------------- /lib/hello_phoenix/guardian_serializer.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.GuardianSerializer do 2 | @behaviour Guardian.Serializer 3 | 4 | alias HelloPhoenix.{Repo, User} 5 | 6 | def for_token(user = %User{}), do: { :ok, "User:#{user.id}" } 7 | def for_token(_), do: { :error, "Unknown resource type" } 8 | 9 | def from_token("User:" <> id), do: { :ok, Repo.get(User, String.to_integer(id)) } 10 | def from_token(_), do: { :error, "Unknown resource type" } 11 | end 12 | -------------------------------------------------------------------------------- /lib/hello_phoenix/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo do 2 | use Ecto.Repo, otp_app: :hello_phoenix 3 | end 4 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :hello_phoenix, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | aliases: aliases, 13 | deps: deps] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [mod: {HelloPhoenix, []}, 21 | applications: [ 22 | :phoenix, 23 | :phoenix_html, 24 | :cowboy, 25 | :logger, 26 | :gettext, 27 | :phoenix_ecto, 28 | :postgrex, 29 | :comeonin 30 | ] 31 | ] 32 | end 33 | 34 | # Specifies which paths to compile per environment. 35 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"] 36 | defp elixirc_paths(_), do: ["lib", "web"] 37 | 38 | # Specifies your project dependencies. 39 | # 40 | # Type `mix help deps` for examples and options. 41 | defp deps do 42 | [ 43 | {:phoenix, "~> 1.1.4"}, 44 | {:postgrex, ">= 0.0.0"}, 45 | {:phoenix_ecto, "~> 3.0.0-beta.2"}, 46 | {:phoenix_html, "~> 2.4"}, 47 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 48 | {:gettext, "~> 0.9"}, 49 | {:cowboy, "~> 1.0"}, 50 | {:comeonin, "~> 2.0"}, 51 | {:guardian, "~> 0.10.0"}, 52 | {:credo, "~> 0.2", only: [:dev, :test]}, 53 | ] 54 | end 55 | 56 | # Aliases are shortcut or tasks specific to the current project. 57 | # For example, to create, migrate and run the seeds file at once: 58 | # 59 | # $ mix ecto.setup 60 | # 61 | # See the documentation for `Mix` for more info on aliases. 62 | defp aliases do 63 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 64 | "ecto.reset": ["ecto.drop", "ecto.setup"]] 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"base64url": {:hex, :base64url, "0.0.1"}, 2 | "bunt": {:hex, :bunt, "0.1.5"}, 3 | "comeonin": {:hex, :comeonin, "2.1.1"}, 4 | "connection": {:hex, :connection, "1.0.2"}, 5 | "cowboy": {:hex, :cowboy, "1.0.4"}, 6 | "cowlib": {:hex, :cowlib, "1.0.2"}, 7 | "credo": {:hex, :credo, "0.3.5"}, 8 | "db_connection": {:hex, :db_connection, "0.2.4"}, 9 | "decimal": {:hex, :decimal, "1.1.1"}, 10 | "ecto": {:hex, :ecto, "2.0.0-beta.1"}, 11 | "fs": {:hex, :fs, "0.9.2"}, 12 | "gettext": {:hex, :gettext, "0.10.0"}, 13 | "guardian": {:hex, :guardian, "0.10.1"}, 14 | "jose": {:hex, :jose, "1.7.0"}, 15 | "phoenix": {:hex, :phoenix, "1.1.4"}, 16 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.0.0-beta.2"}, 17 | "phoenix_html": {:hex, :phoenix_html, "2.5.0"}, 18 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.3"}, 19 | "plug": {:hex, :plug, "1.1.2"}, 20 | "poison": {:hex, :poison, "1.5.2"}, 21 | "poolboy": {:hex, :poolboy, "1.5.1"}, 22 | "postgrex": {:hex, :postgrex, "0.11.1"}, 23 | "ranch": {:hex, :ranch, "1.2.1"}, 24 | "uuid": {:hex, :uuid, "1.1.3"}} 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "dependencies": { 4 | "babel-brunch": "~6.0.0", 5 | "babel-preset-es2015": "^6.6.0", 6 | "babel-preset-react": "^6.3.13", 7 | "babel-preset-stage-0": "^6.3.13", 8 | "babel-preset-stage-2": "^6.3.13", 9 | "bourbon": "^4.2.6", 10 | "bourbon-neat": "^1.7.4", 11 | "brunch": "github:brunch/brunch", 12 | "classnames": "^2.2.3", 13 | "clean-css-brunch": "^2.0.0", 14 | "css-brunch": "^2.0.0", 15 | "es6-promise": "^3.0.2", 16 | "history": "^2.0.1", 17 | "invariant": "^2.2.1", 18 | "isomorphic-fetch": "^2.2.1", 19 | "javascript-brunch": "^2.0.0", 20 | "moment": "^2.12.0", 21 | "phoenix": "file:deps/phoenix", 22 | "phoenix_html": "file:deps/phoenix_html", 23 | "react": "^0.14.7", 24 | "react-addons-css-transition-group": "^0.14.7", 25 | "react-dom": "^0.14.7", 26 | "react-gravatar": "^2.4.0", 27 | "react-redux": "^4.4.1", 28 | "react-router": "^2.0.1", 29 | "react-router-redux": "^4.0.0", 30 | "redux": "^3.1.2", 31 | "redux-logger": "^2.6.1", 32 | "redux-thunk": "^2.0.1", 33 | "sass-brunch": "^2.0.0", 34 | "uglify-js-brunch": "^2.0.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. Do not add, change, or 2 | ## remove `msgid`s manually here as they're tied to the ones in the 3 | ## corresponding POT file (with the same domain). Use `mix gettext.extract 4 | ## --merge` or `mix gettext.merge` to merge POT files into PO files. 5 | msgid "" 6 | msgstr "" 7 | "Language: en\n" 8 | 9 | ## From Ecto.Changeset.cast/4 10 | msgid "can't be blank" 11 | msgstr "" 12 | 13 | ## From Ecto.Changeset.unique_constraint/3 14 | msgid "has already been taken" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.put_change/3 18 | msgid "is invalid" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.validate_format/3 22 | msgid "has invalid format" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_subset/3 26 | msgid "has an invalid entry" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_exclusion/3 30 | msgid "is reserved" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_confirmation/3 34 | msgid "does not match confirmation" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.no_assoc_constraint/3 38 | msgid "is still associated to this entry" 39 | msgstr "" 40 | 41 | msgid "are still associated to this entry" 42 | msgstr "" 43 | 44 | ## From Ecto.Changeset.validate_length/3 45 | msgid "should be %{count} character(s)" 46 | msgid_plural "should be %{count} character(s)" 47 | msgstr[0] "" 48 | msgstr[1] "" 49 | 50 | msgid "should have %{count} item(s)" 51 | msgid_plural "should have %{count} item(s)" 52 | msgstr[0] "" 53 | msgstr[1] "" 54 | 55 | msgid "should be at least %{count} character(s)" 56 | msgid_plural "should be at least %{count} character(s)" 57 | msgstr[0] "" 58 | msgstr[1] "" 59 | 60 | msgid "should have at least %{count} item(s)" 61 | msgid_plural "should have at least %{count} item(s)" 62 | msgstr[0] "" 63 | msgstr[1] "" 64 | 65 | msgid "should be at most %{count} character(s)" 66 | msgid_plural "should be at most %{count} character(s)" 67 | msgstr[0] "" 68 | msgstr[1] "" 69 | 70 | msgid "should have at most %{count} item(s)" 71 | msgid_plural "should have at most %{count} item(s)" 72 | msgstr[0] "" 73 | msgstr[1] "" 74 | 75 | ## From Ecto.Changeset.validate_number/3 76 | msgid "must be less than %{count}" 77 | msgid_plural "must be less than %{count}" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | msgid "must be greater than %{count}" 82 | msgid_plural "must be greater than %{count}" 83 | msgstr[0] "" 84 | msgstr[1] "" 85 | 86 | msgid "must be less than or equal to %{count}" 87 | msgid_plural "must be less than or equal to %{count}" 88 | msgstr[0] "" 89 | msgstr[1] "" 90 | 91 | msgid "must be greater than or equal to %{count}" 92 | msgid_plural "must be greater than or equal to %{count}" 93 | msgstr[0] "" 94 | msgstr[1] "" 95 | 96 | msgid "must be equal to %{count}" 97 | msgid_plural "must be equal to %{count}" 98 | msgstr[0] "" 99 | msgstr[1] "" 100 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. `msgid`s here are often extracted from 2 | ## source code; add new translations manually only if they're dynamic 3 | ## translations that can't be statically extracted. Run `mix 4 | ## gettext.extract` to bring this file up to date. Leave `msgstr`s empty as 5 | ## changing them here as no effect; edit them in PO (`.po`) files instead. 6 | 7 | ## From Ecto.Changeset.cast/4 8 | msgid "can't be blank" 9 | msgstr "" 10 | 11 | ## From Ecto.Changeset.unique_constraint/3 12 | msgid "has already been taken" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.put_change/3 16 | msgid "is invalid" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.validate_format/3 20 | msgid "has invalid format" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_subset/3 24 | msgid "has an invalid entry" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_exclusion/3 28 | msgid "is reserved" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_confirmation/3 32 | msgid "does not match confirmation" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.no_assoc_constraint/3 36 | msgid "is still associated to this entry" 37 | msgstr "" 38 | 39 | msgid "are still associated to this entry" 40 | msgstr "" 41 | 42 | ## From Ecto.Changeset.validate_length/3 43 | msgid "should be %{count} character(s)" 44 | msgid_plural "should be %{count} character(s)" 45 | msgstr[0] "" 46 | msgstr[1] "" 47 | 48 | msgid "should have %{count} item(s)" 49 | msgid_plural "should have %{count} item(s)" 50 | msgstr[0] "" 51 | msgstr[1] "" 52 | 53 | msgid "should be at least %{count} character(s)" 54 | msgid_plural "should be at least %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have at least %{count} item(s)" 59 | msgid_plural "should have at least %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at most %{count} character(s)" 64 | msgid_plural "should be at most %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at most %{count} item(s)" 69 | msgid_plural "should have at most %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | ## From Ecto.Changeset.validate_number/3 74 | msgid "must be less than %{count}" 75 | msgid_plural "must be less than %{count}" 76 | msgstr[0] "" 77 | msgstr[1] "" 78 | 79 | msgid "must be greater than %{count}" 80 | msgid_plural "must be greater than %{count}" 81 | msgstr[0] "" 82 | msgstr[1] "" 83 | 84 | msgid "must be less than or equal to %{count}" 85 | msgid_plural "must be less than or equal to %{count}" 86 | msgstr[0] "" 87 | msgstr[1] "" 88 | 89 | msgid "must be greater than or equal to %{count}" 90 | msgid_plural "must be greater than or equal to %{count}" 91 | msgstr[0] "" 92 | msgstr[1] "" 93 | 94 | msgid "must be equal to %{count}" 95 | msgid_plural "must be equal to %{count}" 96 | msgstr[0] "" 97 | msgstr[1] "" 98 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160129095239_create_user.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Repo.Migrations.CreateUser do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users) do 6 | add :first_name, :string, null: false 7 | add :last_name, :string 8 | add :email, :string, null: false 9 | add :encrypted_password, :string, null: false 10 | 11 | timestamps 12 | end 13 | 14 | create unique_index(:users, [:email]) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /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 | # HelloPhoenix.Repo.insert!(%HelloPhoenix.SomeModel{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /test/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.PageControllerTest do 2 | use HelloPhoenix.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 | -------------------------------------------------------------------------------- /test/models/user_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.UserTest do 2 | use HelloPhoenix.ModelCase 3 | 4 | alias HelloPhoenix.User 5 | 6 | @valid_attrs %{crypted_password: "some content", email: "some content", first_name: "some content", last_name: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = User.changeset(%User{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = User.changeset(%User{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.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 | imports other functionality to make it easier 8 | to build and query models. 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 | alias HelloPhoenix.Repo 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query, only: [from: 1, from: 2] 27 | 28 | 29 | # The default endpoint for testing 30 | @endpoint HelloPhoenix.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | unless tags[:async] do 36 | Ecto.Adapters.SQL.restart_test_transaction(HelloPhoenix.Repo, []) 37 | end 38 | 39 | :ok 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.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 | imports other functionality to make it easier 8 | to build and query models. 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 | 23 | alias HelloPhoenix.Repo 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query, only: [from: 1, from: 2] 27 | 28 | import HelloPhoenix.Router.Helpers 29 | 30 | # The default endpoint for testing 31 | @endpoint HelloPhoenix.Endpoint 32 | end 33 | end 34 | 35 | setup tags do 36 | unless tags[:async] do 37 | Ecto.Adapters.SQL.restart_test_transaction(HelloPhoenix.Repo, []) 38 | end 39 | 40 | {:ok, conn: Phoenix.ConnTest.conn()} 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/support/model_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ModelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | model tests. 5 | 6 | You may define functions here to be used as helpers in 7 | your model tests. See `errors_on/2`'s definition as reference. 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 HelloPhoenix.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query, only: [from: 1, from: 2] 24 | import HelloPhoenix.ModelCase 25 | end 26 | end 27 | 28 | setup tags do 29 | unless tags[:async] do 30 | Ecto.Adapters.SQL.restart_test_transaction(HelloPhoenix.Repo, []) 31 | end 32 | 33 | :ok 34 | end 35 | 36 | @doc """ 37 | Helper for returning list of errors in model when passed certain data. 38 | 39 | ## Examples 40 | 41 | Given a User model that lists `:name` as a required field and validates 42 | `:password` to be safe, it would return: 43 | 44 | iex> errors_on(%User{}, %{password: "password"}) 45 | [password: "is unsafe", name: "is blank"] 46 | 47 | You could then write your assertion like: 48 | 49 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"}) 50 | 51 | You can also create the changeset manually and retrieve the errors 52 | field directly: 53 | 54 | iex> changeset = User.changeset(%User{}, password: "password") 55 | iex> {:password, "is unsafe"} in changeset.errors 56 | true 57 | """ 58 | def errors_on(model, data) do 59 | model.__struct__.changeset(model, data).errors 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | 3 | Mix.Task.run "ecto.create", ~w(-r HelloPhoenix.Repo --quiet) 4 | Mix.Task.run "ecto.migrate", ~w(-r HelloPhoenix.Repo --quiet) 5 | Ecto.Adapters.SQL.begin_test_transaction(HelloPhoenix.Repo) 6 | 7 | -------------------------------------------------------------------------------- /test/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ErrorViewTest do 2 | use HelloPhoenix.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(HelloPhoenix.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(HelloPhoenix.ErrorView, "500.html", []) == 14 | "Server internal error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(HelloPhoenix.ErrorView, "505.html", []) == 19 | "Server internal error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.LayoutViewTest do 2 | use HelloPhoenix.ConnCase, async: true 3 | end -------------------------------------------------------------------------------- /test/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.PageViewTest do 2 | use HelloPhoenix.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /web/channels/user_channel.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.UserChannel do 2 | use HelloPhoenix.Web, :channel 3 | 4 | def join("users:" <> user_id, _params, socket) do 5 | current_user = socket.assigns.current_user 6 | 7 | if String.to_integer(user_id) == current_user.id do 8 | {:ok, socket} 9 | else 10 | {:error, %{reason: "Invalid user"}} 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.UserSocket do 2 | use Phoenix.Socket 3 | 4 | alias HelloPhoenix.{GuardianSerializer} 5 | 6 | # Channels 7 | channel "users:*", HelloPhoenix.UserChannel 8 | 9 | # Transports 10 | transport :websocket, Phoenix.Transports.WebSocket 11 | transport :longpoll, Phoenix.Transports.LongPoll 12 | 13 | def connect(%{"token" => token}, socket) do 14 | case Guardian.decode_and_verify(token) do 15 | {:ok, claims} -> 16 | case GuardianSerializer.from_token(claims["sub"]) do 17 | {:ok, user} -> 18 | {:ok, assign(socket, :current_user, user)} 19 | {:error, _reason} -> 20 | :error 21 | end 22 | {:error, _reason} -> 23 | :error 24 | end 25 | end 26 | 27 | def connect(_params, _socket), do: :error 28 | 29 | def id(socket), do: "users_socket:#{socket.assigns.current_user.id}" 30 | end 31 | -------------------------------------------------------------------------------- /web/controllers/api/v1/current_user_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.CurrentUserController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | plug Guardian.Plug.EnsureAuthenticated, handler: HelloPhoenix.SessionController 5 | 6 | def show(conn, _) do 7 | user = Guardian.Plug.current_resource(conn) 8 | 9 | conn 10 | |> put_status(:ok) 11 | |> render("show.json", user: user) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /web/controllers/api/v1/registration_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.RegistrationController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | alias HelloPhoenix.{Repo, User} 5 | 6 | plug :scrub_params, "user" when action in [:create] 7 | 8 | def create(conn, %{"user" => user_params}) do 9 | changeset = User.changeset(%User{}, user_params) 10 | 11 | case Repo.insert(changeset) do 12 | {:ok, user} -> 13 | {:ok, jwt, _full_claims} = user |> Guardian.encode_and_sign(:token) 14 | 15 | conn 16 | |> put_status(:created) 17 | |> render(HelloPhoenix.SessionView, "show.json", jwt: jwt, user: user) 18 | 19 | {:error, changeset} -> 20 | conn 21 | |> put_status(:unprocessable_entity) 22 | |> render("error.json", changeset: changeset) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /web/controllers/api/v1/session_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.SessionController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | plug :scrub_params, "session" when action in [:create] 5 | 6 | def create(conn, %{"session" => session_params}) do 7 | case HelloPhoenix.Session.authenticate(session_params) do 8 | {:ok, user} -> 9 | {:ok, jwt, _full_claims} = user |> Guardian.encode_and_sign(:token) 10 | 11 | conn 12 | |> put_status(:created) 13 | |> render("show.json", jwt: jwt, user: user) 14 | 15 | :error -> 16 | conn 17 | |> put_status(:unprocessable_entity) 18 | |> render("error.json") 19 | end 20 | end 21 | 22 | def delete(conn, _) do 23 | {:ok, claims} = Guardian.Plug.claims(conn) 24 | 25 | conn 26 | |> Guardian.Plug.current_token 27 | |> Guardian.revoke!(claims) 28 | 29 | conn 30 | |> render("delete.json") 31 | end 32 | 33 | def unauthenticated(conn, _params) do 34 | conn 35 | |> put_status(:forbidden) 36 | |> render(HelloPhoenix.SessionView, "forbidden.json", error: "Not Authenticated") 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.PageController do 2 | use HelloPhoenix.Web, :controller 3 | 4 | def index(conn, _params) do 5 | render conn, "index.html" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](http://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import HelloPhoenix.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](http://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :hello_phoenix 24 | end 25 | -------------------------------------------------------------------------------- /web/helpers/session.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Session do 2 | alias HelloPhoenix.{Repo, User} 3 | 4 | def authenticate(%{"email" => email, "password" => password}) do 5 | user = Repo.get_by(User, email: String.downcase(email)) 6 | 7 | case check_password(user, password) do 8 | true -> {:ok, user} 9 | _ -> :error 10 | end 11 | end 12 | 13 | defp check_password(user, password) do 14 | case user do 15 | nil -> Comeonin.Bcrypt.dummy_checkpw() 16 | _ -> Comeonin.Bcrypt.checkpw(password, user.encrypted_password) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /web/models/user.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.User do 2 | use HelloPhoenix.Web, :model 3 | 4 | @derive {Poison.Encoder, only: [:id, :first_name, :last_name, :email]} 5 | 6 | schema "users" do 7 | field :first_name, :string 8 | field :last_name, :string 9 | field :email, :string 10 | field :encrypted_password, :string 11 | 12 | field :password, :string, virtual: true 13 | 14 | timestamps 15 | end 16 | 17 | @required_fields ~w(first_name email password) 18 | @optional_fields ~w(encrypted_password last_name) 19 | 20 | @doc """ 21 | Creates a changeset based on the `model` and `params`. 22 | 23 | If no params are provided, an invalid changeset is returned 24 | with no validation performed. 25 | """ 26 | def changeset(model, params \\ :empty) do 27 | model 28 | |> cast(params, @required_fields, @optional_fields) 29 | |> validate_format(:email, ~r/@/) 30 | |> validate_length(:password, min: 5) 31 | |> unique_constraint(:email, message: "Email already taken") 32 | |> generate_encrypted_password 33 | end 34 | 35 | defp generate_encrypted_password(current_changeset) do 36 | case current_changeset do 37 | %Ecto.Changeset{valid?: true, changes: %{password: password}} -> 38 | put_change(current_changeset, :encrypted_password, Comeonin.Bcrypt.hashpwsalt(password)) 39 | _ -> 40 | current_changeset 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Router do 2 | use HelloPhoenix.Web, :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 | plug Guardian.Plug.VerifyHeader 15 | plug Guardian.Plug.LoadResource 16 | end 17 | 18 | scope "/api", HelloPhoenix do 19 | pipe_through :api 20 | 21 | scope "/v1" do 22 | post "/registrations", RegistrationController, :create 23 | 24 | post "/sessions", SessionController, :create 25 | delete "/sessions", SessionController, :delete 26 | 27 | get "/current_user", CurrentUserController, :show 28 | end 29 | end 30 | 31 | scope "/", HelloPhoenix do 32 | pipe_through :browser # Use the default browser stack 33 | 34 | get "*path", PageController, :index 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /web/static/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigardone/phoenix-react-redux-template/64fd3b9839a4d2777b3d174e59f705069992288e/web/static/assets/favicon.ico -------------------------------------------------------------------------------- /web/static/assets/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigardone/phoenix-react-redux-template/64fd3b9839a4d2777b3d174e59f705069992288e/web/static/assets/images/phoenix.png -------------------------------------------------------------------------------- /web/static/assets/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 | -------------------------------------------------------------------------------- /web/static/css-burrito-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "root": { 4 | "pathToSassDirectory": "./", 5 | "sassDirectory": "stylesheets", 6 | "sassImportFile": "application.scss" 7 | }, 8 | "modules": { 9 | "modulesDirectory": "modules", 10 | "modulesImportFile": "_modules.scss", 11 | "moduleFiles": [ 12 | "_example-module.scss" 13 | ] 14 | } 15 | }, 16 | "template": { 17 | "directories": [ 18 | "global", 19 | "libs" 20 | ], 21 | "files": [ 22 | "global/_base.scss", 23 | "global/_layout.scss", 24 | "global/_settings.scss", 25 | "global/_skin.scss", 26 | "global/_typography.scss", 27 | "global/_utilities.scss", 28 | "libs/_library-variable-overrides.scss", 29 | "libs/_normalize.scss" 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /web/static/css/app.sass: -------------------------------------------------------------------------------- 1 | // - - css-burrito v1.5 | mit license | github.com/jasonreece/css-burrito 2 | 3 | // - - - - - - - - - - - - - - - - - - - 4 | // - - application.scss 5 | // contains an import section for libs, global, and modules, the inbox, 6 | // and a shame section for quick fixes and hacks. 7 | 8 | // - - reset - normalize.css v3.0.2 9 | @import libs/normalize 10 | 11 | // - - - - - - - - - - - - - - - - - - - 12 | // - - libs 13 | // add css frameworks and libraries here. 14 | // be sure to load them after the variable overrides file 15 | // if you want to change variable values. 16 | @import bourbon/app/assets/stylesheets/bourbon 17 | @import bourbon-neat/app/assets/stylesheets/neat 18 | @import bitters/base 19 | 20 | // - - variable overrides for sass libraries 21 | // @import "libs/library-variable-overrides"; 22 | 23 | // - - - - - - - - - - - - - - - - - - - 24 | // - - global 25 | 26 | // - - global/settings 27 | // @font-face declarations, variables 28 | @import global/settings 29 | 30 | // - - global/utilities 31 | // extends, functions, and mixins 32 | @import global/utilities 33 | 34 | // - - global/base 35 | // base-level tags (body, p, etc.) 36 | @import global/base 37 | 38 | // - - global/layout 39 | // margin, padding, sizing 40 | @import global/layout 41 | 42 | // - - global/skin 43 | // backgrounds, borders, box-shadow, etc 44 | @import global/skin 45 | 46 | // - - global/typography 47 | // fonts and colors 48 | @import global/typography 49 | 50 | // - - - - - - - - - - - - - - - - - - - 51 | // - - modules 52 | // add new modules to the modules/_modules.scss file and they'll get pulled in here. 53 | @import modules/modules 54 | 55 | // - - - - - - - - - - - - - - - - - - - 56 | // - - inbox 57 | // the inbox allows developers, and those not actively working on the project 58 | // to quickly add styles that are easily seen by the maintainer of the file. 59 | 60 | // - - - - - - - - - - - - - - - - - - - 61 | // - - shame 62 | // need to add a quick fix, hack, or questionable technique? add it here, fix it later. 63 | -------------------------------------------------------------------------------- /web/static/css/bitters/_base.sass: -------------------------------------------------------------------------------- 1 | // Bitters 1.2.0 2 | // http://bitters.bourbon.io 3 | // Copyright 2013-2015 thoughtbot, inc. 4 | // MIT License 5 | 6 | @import variables 7 | 8 | // Neat Settings -- uncomment if using Neat -- must be imported before Neat 9 | // @import "grid-settings"; 10 | 11 | @import buttons 12 | @import forms 13 | @import lists 14 | @import tables 15 | @import typography 16 | -------------------------------------------------------------------------------- /web/static/css/bitters/_buttons.sass: -------------------------------------------------------------------------------- 1 | #{$all-buttons} 2 | appearance: none 3 | background-color: $action-color 4 | border: 0 5 | border-radius: $base-border-radius 6 | color: #fff 7 | cursor: pointer 8 | display: inline-block 9 | font-family: $base-font-family 10 | font-size: $base-font-size 11 | -webkit-font-smoothing: antialiased 12 | font-weight: 600 13 | line-height: 1 14 | padding: $small-spacing $base-spacing 15 | text-decoration: none 16 | transition: background-color $base-duration $base-timing 17 | user-select: none 18 | vertical-align: middle 19 | white-space: nowrap 20 | 21 | &:hover, 22 | &:focus 23 | background-color: shade($action-color, 20%) 24 | color: #fff 25 | 26 | &:disabled 27 | cursor: not-allowed 28 | opacity: 0.5 29 | 30 | &:hover 31 | background-color: $action-color 32 | -------------------------------------------------------------------------------- /web/static/css/bitters/_forms.sass: -------------------------------------------------------------------------------- 1 | fieldset 2 | background-color: transparent 3 | border: 0 4 | margin: 0 5 | padding: 0 6 | 7 | legend 8 | font-weight: 600 9 | margin-bottom: $small-spacing / 2 10 | padding: 0 11 | 12 | label 13 | display: block 14 | font-weight: 600 15 | margin-bottom: $small-spacing / 2 16 | 17 | input, 18 | select 19 | display: block 20 | font-family: $base-font-family 21 | font-size: $base-font-size 22 | 23 | #{$all-text-inputs}, 24 | select[multiple] 25 | background-color: $base-background-color 26 | border: $base-border 27 | border-radius: $base-border-radius 28 | box-shadow: $form-box-shadow 29 | box-sizing: border-box 30 | font-family: $base-font-family 31 | font-size: $base-font-size 32 | margin-bottom: $small-spacing 33 | padding: $base-spacing / 3 34 | transition: border-color $base-duration $base-timing 35 | width: 100% 36 | 37 | &:hover 38 | border-color: shade($base-border-color, 20%) 39 | 40 | &:focus 41 | border-color: $action-color 42 | box-shadow: $form-box-shadow-focus 43 | outline: none 44 | 45 | &:disabled 46 | background-color: shade($base-background-color, 5%) 47 | cursor: not-allowed 48 | 49 | &:hover 50 | border: $base-border 51 | 52 | textarea 53 | resize: vertical 54 | 55 | [type="search"] 56 | appearance: none 57 | 58 | [type="checkbox"], 59 | [type="radio"] 60 | display: inline 61 | margin-right: $small-spacing / 2 62 | 63 | [type="file"] 64 | margin-bottom: $small-spacing 65 | width: 100% 66 | 67 | select 68 | margin-bottom: $base-spacing 69 | max-width: 100% 70 | width: auto 71 | -------------------------------------------------------------------------------- /web/static/css/bitters/_grid-settings.sass: -------------------------------------------------------------------------------- 1 | @import neat-helpers 2 | 3 | // or "../neat/neat-helpers" when not in Rails 4 | 5 | // Neat Overrides 6 | // $column: 90px; 7 | // $gutter: 30px; 8 | // $grid-columns: 12; 9 | // $max-width: 1200px; 10 | 11 | // Neat Breakpoints 12 | $medium-screen: 600px 13 | $large-screen: 900px 14 | 15 | $medium-screen-up: new-breakpoint(min-width $medium-screen 4) 16 | $large-screen-up: new-breakpoint(min-width $large-screen 8) 17 | -------------------------------------------------------------------------------- /web/static/css/bitters/_lists.sass: -------------------------------------------------------------------------------- 1 | ul, 2 | ol 3 | list-style-type: none 4 | margin: 0 5 | padding: 0 6 | 7 | dl 8 | margin-bottom: $small-spacing 9 | 10 | dt 11 | font-weight: 600 12 | margin-top: $small-spacing 13 | 14 | dd 15 | margin: 0 16 | -------------------------------------------------------------------------------- /web/static/css/bitters/_tables.sass: -------------------------------------------------------------------------------- 1 | table 2 | border-collapse: collapse 3 | margin: $small-spacing 0 4 | table-layout: fixed 5 | width: 100% 6 | 7 | th 8 | border-bottom: 1px solid shade($base-border-color, 25%) 9 | font-weight: 600 10 | padding: $small-spacing 0 11 | text-align: left 12 | 13 | td 14 | border-bottom: $base-border 15 | padding: $small-spacing 0 16 | 17 | tr, 18 | td, 19 | th 20 | vertical-align: middle 21 | -------------------------------------------------------------------------------- /web/static/css/bitters/_typography.sass: -------------------------------------------------------------------------------- 1 | body 2 | color: $base-font-color 3 | font-family: $base-font-family 4 | font-size: $base-font-size 5 | line-height: $base-line-height 6 | 7 | h1, 8 | h2, 9 | h3, 10 | h4, 11 | h5, 12 | h6 13 | font-family: $heading-font-family 14 | font-size: $base-font-size 15 | line-height: $heading-line-height 16 | margin: 0 0 $small-spacing 17 | 18 | p 19 | margin: 0 0 $small-spacing 20 | 21 | a 22 | color: $action-color 23 | text-decoration: none 24 | transition: color $base-duration $base-timing 25 | 26 | &:active, 27 | &:focus, 28 | &:hover 29 | color: shade($action-color, 25%) 30 | 31 | hr 32 | border-bottom: $base-border 33 | border-left: 0 34 | border-right: 0 35 | border-top: 0 36 | margin: $base-spacing 0 37 | 38 | img, 39 | picture 40 | margin: 0 41 | max-width: 100% 42 | -------------------------------------------------------------------------------- /web/static/css/bitters/_variables.sass: -------------------------------------------------------------------------------- 1 | // Typography 2 | $base-font-family: $helvetica 3 | $heading-font-family: $base-font-family 4 | 5 | // Font Sizes 6 | $base-font-size: 1em 7 | 8 | // Line height 9 | $base-line-height: 1.5 10 | $heading-line-height: 1.2 11 | 12 | // Other Sizes 13 | $base-border-radius: 3px 14 | $base-spacing: $base-line-height * 1em 15 | $small-spacing: $base-spacing / 2 16 | $base-z-index: 0 17 | 18 | // Colors 19 | $blue: #1565c0 20 | $dark-gray: #333 21 | $medium-gray: #999 22 | $light-gray: #ddd 23 | 24 | // Font Colors 25 | $base-font-color: $dark-gray 26 | $action-color: $blue 27 | 28 | // Border 29 | $base-border-color: $light-gray 30 | $base-border: 1px solid $base-border-color 31 | 32 | // Background Colors 33 | $base-background-color: #fff 34 | $secondary-background-color: tint($base-border-color, 75%) 35 | 36 | // Forms 37 | $form-box-shadow: 0 0 0 38 | $form-box-shadow-focus: 0 0 0 39 | 40 | // Animations 41 | $base-duration: 150ms 42 | $base-timing: ease 43 | -------------------------------------------------------------------------------- /web/static/css/global/_base.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - base 3 | // project defaults for base elements - h1-h6, p, a, etc. 4 | -------------------------------------------------------------------------------- /web/static/css/global/_layout.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - layout 3 | // global layout classes - height, width, padding, margin, etc. 4 | -------------------------------------------------------------------------------- /web/static/css/global/_settings.sass: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/static/css/global/_skin.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - skin 3 | // global skin styles - gradients, colors, box-shadows, etc. 4 | -------------------------------------------------------------------------------- /web/static/css/global/_typography.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - typography 3 | // global typography styles 4 | -------------------------------------------------------------------------------- /web/static/css/global/_utilities.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - utilities 3 | // placeholders extends, mixins, functions, and utility classes 4 | 5 | // - - - - - - - - - - - - - - - - - - - 6 | // - - placeholder extends 7 | 8 | // - - - - - - - - - - - - - - - - - - - 9 | // - - mixins 10 | 11 | // - - clearfix 12 | // @mixin clearfix { 13 | // &:before, 14 | // &:after { 15 | // content: ' '; 16 | // display: table; 17 | // } 18 | // &:after { 19 | // clear: both; 20 | // } 21 | // & { 22 | // *zoom: 1; 23 | // } 24 | // } 25 | 26 | // - - breakpoint 27 | // adds responsive breakpoints. 28 | // @mixin breakpoint($width) { 29 | // @media (min-width: $width) { 30 | // @content; 31 | // } 32 | // } 33 | 34 | // - - attention 35 | // adds accessibility pseudo selectors to hover states. 36 | // @mixin attention() { 37 | // &:hover, 38 | // &:active, 39 | // &:focus { 40 | // @content; 41 | // } 42 | // } 43 | 44 | // - - - - - - - - - - - - - - - - - - - 45 | // - - functions 46 | 47 | // - - - - - - - - - - - - - - - - - - - 48 | // - - utilities 49 | -------------------------------------------------------------------------------- /web/static/css/libs/_library-variable-overrides.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - library variable overrides 3 | // variable overrides for css frameworks and libraries 4 | -------------------------------------------------------------------------------- /web/static/css/libs/_normalize.sass: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | 8 | html 9 | font-family: sans-serif 10 | 11 | /* 1 12 | -ms-text-size-adjust: 100% 13 | 14 | /* 2 15 | -webkit-text-size-adjust: 100% 16 | 17 | /* 2 18 | 19 | /** 20 | * Remove default margin. 21 | 22 | body 23 | margin: 0 24 | 25 | /* HTML5 display definitions 26 | * ========================================================================== 27 | 28 | /** 29 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 30 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 31 | * and Firefox. 32 | * Correct `block` display not defined for `main` in IE 11. 33 | 34 | article, 35 | aside, 36 | details, 37 | figcaption, 38 | figure, 39 | footer, 40 | header, 41 | hgroup, 42 | main, 43 | menu, 44 | nav, 45 | section, 46 | summary 47 | display: block 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | 53 | audio, 54 | canvas, 55 | progress, 56 | video 57 | display: inline-block 58 | 59 | /* 1 60 | vertical-align: baseline 61 | 62 | /* 2 63 | 64 | /** 65 | * Prevent modern browsers from displaying `audio` without controls. 66 | * Remove excess height in iOS 5 devices. 67 | 68 | audio:not([controls]) 69 | display: none 70 | height: 0 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | 76 | [hidden], 77 | template 78 | display: none 79 | 80 | /* Links 81 | * ========================================================================== 82 | 83 | /** 84 | * Remove the gray background color from active links in IE 10. 85 | 86 | a 87 | background-color: transparent 88 | 89 | /** 90 | * Improve readability when focused and also mouse hovered in all browsers. 91 | 92 | a:active, 93 | a:hover 94 | outline: 0 95 | 96 | /* Text-level semantics 97 | * ========================================================================== 98 | 99 | /** 100 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 101 | 102 | abbr[title] 103 | border-bottom: 1px dotted 104 | 105 | /** 106 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 107 | 108 | b, 109 | strong 110 | font-weight: bold 111 | 112 | /** 113 | * Address styling not present in Safari and Chrome. 114 | 115 | dfn 116 | font-style: italic 117 | 118 | /** 119 | * Address variable `h1` font-size and margin within `section` and `article` 120 | * contexts in Firefox 4+, Safari, and Chrome. 121 | 122 | h1 123 | font-size: 2em 124 | margin: 0.67em 0 125 | 126 | /** 127 | * Address styling not present in IE 8/9. 128 | 129 | mark 130 | background: #ff0 131 | color: #000 132 | 133 | /** 134 | * Address inconsistent and variable font size in all browsers. 135 | 136 | small 137 | font-size: 80% 138 | 139 | /** 140 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 141 | 142 | sub, 143 | sup 144 | font-size: 75% 145 | line-height: 0 146 | position: relative 147 | vertical-align: baseline 148 | 149 | sup 150 | top: -0.5em 151 | 152 | sub 153 | bottom: -0.25em 154 | 155 | /* Embedded content 156 | * ========================================================================== 157 | 158 | /** 159 | * Remove border when inside `a` element in IE 8/9/10. 160 | 161 | img 162 | border: 0 163 | 164 | /** 165 | * Correct overflow not hidden in IE 9/10/11. 166 | 167 | svg:not(:root) 168 | overflow: hidden 169 | 170 | /* Grouping content 171 | * ========================================================================== 172 | 173 | /** 174 | * Address margin not present in IE 8/9 and Safari. 175 | 176 | figure 177 | margin: 1em 40px 178 | 179 | /** 180 | * Address differences between Firefox and other browsers. 181 | 182 | hr 183 | -moz-box-sizing: content-box 184 | box-sizing: content-box 185 | height: 0 186 | 187 | /** 188 | * Contain overflow in all browsers. 189 | 190 | pre 191 | overflow: auto 192 | 193 | /** 194 | * Address odd `em`-unit font size rendering in all browsers. 195 | 196 | code, 197 | kbd, 198 | pre, 199 | samp 200 | font-family: monospace, monospace 201 | font-size: 1em 202 | 203 | /* Forms 204 | * ========================================================================== 205 | 206 | /** 207 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 208 | * styling of `select`, unless a `border` property is set. 209 | 210 | /** 211 | * 1. Correct color not being inherited. 212 | * Known issue: affects color of disabled elements. 213 | * 2. Correct font properties not being inherited. 214 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 215 | 216 | button, 217 | input, 218 | optgroup, 219 | select, 220 | textarea 221 | color: inherit 222 | 223 | /* 1 224 | font: inherit 225 | 226 | /* 2 227 | margin: 0 228 | 229 | /* 3 230 | 231 | /** 232 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 233 | 234 | button 235 | overflow: visible 236 | 237 | /** 238 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 239 | * All other form control elements do not inherit `text-transform` values. 240 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 241 | * Correct `select` style inheritance in Firefox. 242 | 243 | button, 244 | select 245 | text-transform: none 246 | 247 | /** 248 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 249 | * and `video` controls. 250 | * 2. Correct inability to style clickable `input` types in iOS. 251 | * 3. Improve usability and consistency of cursor style between image-type 252 | * `input` and others. 253 | 254 | button, 255 | html input[type="button"], 256 | input[type="reset"], 257 | input[type="submit"] 258 | -webkit-appearance: button 259 | 260 | /* 2 261 | cursor: pointer 262 | 263 | /* 3 264 | 265 | /** 266 | * Re-set default cursor for disabled elements. 267 | 268 | button[disabled], 269 | html input[disabled] 270 | cursor: default 271 | 272 | /** 273 | * Remove inner padding and border in Firefox 4+. 274 | 275 | button::-moz-focus-inner, 276 | input::-moz-focus-inner 277 | border: 0 278 | padding: 0 279 | 280 | /** 281 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 282 | * the UA stylesheet. 283 | 284 | input 285 | line-height: normal 286 | 287 | /** 288 | * It's recommended that you don't attempt to style these elements. 289 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 290 | * 291 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 292 | * 2. Remove excess padding in IE 8/9/10. 293 | 294 | input[type="checkbox"], 295 | input[type="radio"] 296 | box-sizing: border-box 297 | 298 | /* 1 299 | padding: 0 300 | 301 | /* 2 302 | 303 | /** 304 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 305 | * `font-size` values of the `input`, it causes the cursor style of the 306 | * decrement button to change from `default` to `text`. 307 | 308 | input[type="number"]::-webkit-inner-spin-button, 309 | input[type="number"]::-webkit-outer-spin-button 310 | height: auto 311 | 312 | /** 313 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 314 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 315 | * (include `-moz` to future-proof). 316 | 317 | input[type="search"] 318 | -webkit-appearance: textfield 319 | 320 | /* 1 321 | -moz-box-sizing: content-box 322 | -webkit-box-sizing: content-box 323 | 324 | /* 2 325 | box-sizing: content-box 326 | 327 | /** 328 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 329 | * Safari (but not Chrome) clips the cancel button when the search input has 330 | * padding (and `textfield` appearance). 331 | 332 | input[type="search"]::-webkit-search-cancel-button, 333 | input[type="search"]::-webkit-search-decoration 334 | -webkit-appearance: none 335 | 336 | /** 337 | * Define consistent border, margin, and padding. 338 | 339 | fieldset 340 | border: 1px solid #c0c0c0 341 | margin: 0 2px 342 | padding: 0.35em 0.625em 0.75em 343 | 344 | /** 345 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 346 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 347 | 348 | legend 349 | border: 0 350 | 351 | /* 1 352 | padding: 0 353 | 354 | /* 2 355 | 356 | /** 357 | * Remove default vertical scrollbar in IE 8/9/10/11. 358 | 359 | textarea 360 | overflow: auto 361 | 362 | /** 363 | * Don't inherit the `font-weight` (applied by a rule above). 364 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 365 | 366 | optgroup 367 | font-weight: bold 368 | 369 | /* Tables 370 | * ========================================================================== 371 | 372 | /** 373 | * Remove most spacing between table cells. 374 | 375 | table 376 | border-collapse: collapse 377 | border-spacing: 0 378 | 379 | td, 380 | th 381 | padding: 0 382 | -------------------------------------------------------------------------------- /web/static/css/modules/_example-module.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - example-module module 3 | // styles for the example-module module 4 | -------------------------------------------------------------------------------- /web/static/css/modules/_modules.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - modules 3 | // add new modules here 4 | 5 | // @import "example-module"; 6 | -------------------------------------------------------------------------------- /web/static/js/actions/header.js: -------------------------------------------------------------------------------- 1 | import Constants from '../constants'; 2 | import { push } from 'react-router-redux'; 3 | import { httpGet, httpPost } from '../utils'; 4 | 5 | const Actions = { 6 | 7 | }; 8 | 9 | export default Actions; 10 | -------------------------------------------------------------------------------- /web/static/js/actions/registrations.js: -------------------------------------------------------------------------------- 1 | import { push } from 'react-router-redux'; 2 | import Constants from '../constants'; 3 | import { httpPost } from '../utils'; 4 | import {setCurrentUser} from './sessions'; 5 | 6 | const Actions = {}; 7 | 8 | Actions.signUp = (data) => { 9 | return dispatch => { 10 | httpPost('/api/v1/registrations', { user: data }) 11 | .then((data) => { 12 | localStorage.setItem('phoenixAuthToken', data.jwt); 13 | 14 | setCurrentUser(dispatch, data.user); 15 | 16 | dispatch(push('/')); 17 | }) 18 | .catch((error) => { 19 | error.response.json() 20 | .then((errorJSON) => { 21 | dispatch({ 22 | type: Constants.REGISTRATIONS_ERROR, 23 | errors: errorJSON.errors, 24 | }); 25 | }); 26 | }); 27 | }; 28 | }; 29 | 30 | export default Actions; 31 | -------------------------------------------------------------------------------- /web/static/js/actions/sessions.js: -------------------------------------------------------------------------------- 1 | import { push } from 'react-router-redux'; 2 | import Constants from '../constants'; 3 | import { Socket } from 'phoenix'; 4 | import { httpGet, httpPost, httpDelete } from '../utils'; 5 | 6 | export function setCurrentUser(dispatch, user) { 7 | const socket = new Socket('/socket', { 8 | params: { token: localStorage.getItem('phoenixAuthToken') }, 9 | logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data); }, 10 | }); 11 | 12 | socket.connect(); 13 | 14 | const channel = socket.channel(`users:${user.id}`); 15 | 16 | if (channel.state != 'joined') { 17 | channel.join().receive('ok', () => { 18 | dispatch({ 19 | type: Constants.CURRENT_USER, 20 | currentUser: user, 21 | socket: socket, 22 | channel: channel, 23 | }); 24 | }); 25 | } 26 | }; 27 | 28 | const Actions = { 29 | signIn: (email, password) => { 30 | return dispatch => { 31 | const data = { 32 | session: { 33 | email: email, 34 | password: password, 35 | }, 36 | }; 37 | 38 | httpPost('/api/v1/sessions', data) 39 | .then((data) => { 40 | localStorage.setItem('phoenixAuthToken', data.jwt); 41 | setCurrentUser(dispatch, data.user); 42 | dispatch(push('/')); 43 | }) 44 | .catch((error) => { 45 | error.response.json() 46 | .then((errorJSON) => { 47 | dispatch({ 48 | type: Constants.SESSIONS_ERROR, 49 | error: errorJSON.error, 50 | }); 51 | }); 52 | }); 53 | }; 54 | }, 55 | 56 | currentUser: () => { 57 | return dispatch => { 58 | const authToken = localStorage.getItem('phoenixAuthToken'); 59 | 60 | httpGet('/api/v1/current_user') 61 | .then(function (data) { 62 | setCurrentUser(dispatch, data); 63 | }) 64 | .catch(function (error) { 65 | console.log(error); 66 | dispatch(push('/sign_in')); 67 | }); 68 | }; 69 | }, 70 | 71 | signOut: (socket, channel) => { 72 | return dispatch => { 73 | httpDelete('/api/v1/sessions') 74 | .then((data) => { 75 | localStorage.removeItem('phoenixAuthToken'); 76 | 77 | channel.leave(); 78 | socket.disconnect(); 79 | 80 | dispatch({ type: Constants.USER_SIGNED_OUT, }); 81 | 82 | dispatch(push('/sign_in')); 83 | }) 84 | .catch(function (error) { 85 | console.log(error); 86 | }); 87 | }; 88 | }, 89 | }; 90 | 91 | export default Actions; 92 | -------------------------------------------------------------------------------- /web/static/js/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { browserHistory } from 'react-router'; 4 | import { syncHistoryWithStore } from 'react-router-redux'; 5 | import configureStore from './store'; 6 | import Root from './containers/root'; 7 | 8 | const store = configureStore(browserHistory); 9 | const history = syncHistoryWithStore(browserHistory, store); 10 | 11 | const target = document.getElementById('main_container'); 12 | const node = ; 13 | 14 | ReactDOM.render(node, target); 15 | -------------------------------------------------------------------------------- /web/static/js/constants/index.js: -------------------------------------------------------------------------------- 1 | const Constants = { 2 | USER_SIGNED_IN: 'USER_SIGNED_IN', 3 | CURRENT_USER: 'CURRENT_USER', 4 | USER_SIGNED_OUT: 'USER_SIGNED_OUT', 5 | SESSIONS_ERROR: 'SESSIONS_ERROR', 6 | REGISTRATIONS_ERROR: 'REGISTRATIONS_ERROR', 7 | }; 8 | 9 | export default Constants; 10 | -------------------------------------------------------------------------------- /web/static/js/containers/authenticated.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Header from '../layouts/header'; 4 | 5 | class AuthenticatedContainer extends React.Component { 6 | render() { 7 | const { currentUser, dispatch, socket, channel } = this.props; 8 | 9 | if (!currentUser) return false; 10 | 11 | return ( 12 |
13 |
18 | 19 |
20 | {this.props.children} 21 |
22 |
23 | ); 24 | } 25 | } 26 | 27 | const mapStateToProps = (state) => ( 28 | state.session 29 | ); 30 | 31 | export default connect(mapStateToProps)(AuthenticatedContainer); 32 | -------------------------------------------------------------------------------- /web/static/js/containers/root.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { Router } from 'react-router'; 4 | import invariant from 'invariant'; 5 | import { RoutingContext } from 'react-router'; 6 | import configRoutes from '../routes'; 7 | 8 | export default class Root extends React.Component { 9 | _renderRouter(store) { 10 | invariant(this.props.routerHistory, 11 | ' needs either a routingContext or routerHistory to render.' 12 | ); 13 | 14 | return ( 15 | 16 | {configRoutes(store)} 17 | 18 | ); 19 | } 20 | 21 | render() { 22 | const { store } = this.props; 23 | 24 | return ( 25 | 26 | {this._renderRouter(store)} 27 | 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /web/static/js/layouts/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | import ReactGravatar from 'react-gravatar'; 5 | import { push} from 'react-router-redux'; 6 | 7 | import SessionActions from '../actions/sessions'; 8 | import HeaderActions from '../actions/header'; 9 | 10 | export default class Header extends React.Component { 11 | _renderCurrentUser() { 12 | const { currentUser } = this.props; 13 | const fullName = [currentUser.first_name, currentUser.last_name].join(' '); 14 | 15 | return ( 16 | 17 | {fullName} 18 | 19 | ); 20 | } 21 | 22 | _renderSignOutLink() { 23 | if (!this.props.currentUser) { 24 | return false; 25 | } 26 | 27 | return ( 28 | Sign out 29 | ); 30 | } 31 | 32 | _handleSignOutClick(e) { 33 | e.preventDefault(); 34 | 35 | const { dispatch, socket, channel } = this.props; 36 | 37 | dispatch(SessionActions.signOut(socket, channel)); 38 | } 39 | 40 | render() { 41 | return ( 42 |
43 | 44 | 45 | 46 |
47 |
    48 |
  • 49 | {this._renderCurrentUser()} 50 |
  • 51 |
  • 52 | {this._renderSignOutLink()} 53 |
  • 54 |
55 |
56 |
57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /web/static/js/layouts/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | export default class MainLayout extends React.Component { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | render() { 10 | return ( 11 |
12 | {this.props.children} 13 |
14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /web/static/js/reducers/header.js: -------------------------------------------------------------------------------- 1 | import Constants from '../constants'; 2 | 3 | const initialState = { 4 | }; 5 | 6 | export default function reducer(state = initialState, action = {}) { 7 | switch (action.type) { 8 | 9 | default: 10 | return state; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /web/static/js/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import session from './session'; 4 | import registration from './registration'; 5 | import header from './header'; 6 | 7 | export default combineReducers({ 8 | routing: routerReducer, 9 | session: session, 10 | registration: registration, 11 | header: header, 12 | }); 13 | -------------------------------------------------------------------------------- /web/static/js/reducers/registration.js: -------------------------------------------------------------------------------- 1 | import Constants from '../constants'; 2 | 3 | const initialState = { 4 | errors: null, 5 | }; 6 | 7 | export default function reducer(state = initialState, action = {}) { 8 | switch (action.type) { 9 | case Constants.REGISTRATIONS_ERROR: 10 | return { ...state, errors: action.errors }; 11 | 12 | default: 13 | return state; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /web/static/js/reducers/session.js: -------------------------------------------------------------------------------- 1 | import Constants from '../constants'; 2 | 3 | const initialState = { 4 | currentUser: null, 5 | socket: null, 6 | channel: null, 7 | error: null, 8 | }; 9 | 10 | export default function reducer(state = initialState, action = {}) { 11 | switch (action.type) { 12 | case Constants.CURRENT_USER: 13 | return { 14 | ...state, 15 | currentUser: action.currentUser, 16 | socket: action.socket, 17 | channel: action.channel, 18 | error: null, 19 | }; 20 | 21 | case Constants.USER_SIGNED_OUT: 22 | return initialState; 23 | 24 | case Constants.SESSIONS_ERROR: 25 | return { ...state, error: action.error }; 26 | 27 | default: 28 | return state; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /web/static/js/routes/index.js: -------------------------------------------------------------------------------- 1 | import { IndexRoute, Route } from 'react-router'; 2 | import React from 'react'; 3 | import MainLayout from '../layouts/main'; 4 | import AuthenticatedContainer from '../containers/authenticated'; 5 | import HomeIndexView from '../views/home'; 6 | import RegistrationsNew from '../views/registrations/new'; 7 | import SessionsNew from '../views/sessions/new'; 8 | import Actions from '../actions/sessions'; 9 | 10 | export default function configRoutes(store) { 11 | const _ensureAuthenticated = (nextState, replace, callback) => { 12 | const { dispatch } = store; 13 | const { session } = store.getState(); 14 | const { currentUser } = session; 15 | 16 | if (!currentUser && localStorage.getItem('phoenixAuthToken')) { 17 | dispatch(Actions.currentUser()); 18 | } else if (!localStorage.getItem('phoenixAuthToken')) { 19 | replace('/sign_in'); 20 | } 21 | 22 | callback(); 23 | }; 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /web/static/js/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import createLogger from 'redux-logger'; 3 | import thunkMiddleware from 'redux-thunk'; 4 | import { routerMiddleware } from 'react-router-redux'; 5 | import reducers from '../reducers'; 6 | 7 | const loggerMiddleware = createLogger({ 8 | level: 'info', 9 | collapsed: true, 10 | }); 11 | 12 | export default function configureStore(browserHistory) { 13 | const reduxRouterMiddleware = routerMiddleware(browserHistory); 14 | const createStoreWithMiddleware = applyMiddleware(reduxRouterMiddleware, thunkMiddleware, loggerMiddleware)(createStore); 15 | 16 | return createStoreWithMiddleware(reducers); 17 | } 18 | -------------------------------------------------------------------------------- /web/static/js/utils/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import fetch from 'isomorphic-fetch'; 3 | import { polyfill } from 'es6-promise'; 4 | 5 | const defaultHeaders = { 6 | Accept: 'application/json', 7 | 'Content-Type': 'application/json', 8 | }; 9 | 10 | function buildHeaders() { 11 | const authToken = localStorage.getItem('phoenixAuthToken'); 12 | 13 | return { ...defaultHeaders, Authorization: authToken }; 14 | } 15 | 16 | export function checkStatus(response) { 17 | if (response.status >= 200 && response.status < 300) { 18 | return response; 19 | } else { 20 | var error = new Error(response.statusText); 21 | error.response = response; 22 | throw error; 23 | } 24 | } 25 | 26 | export function parseJSON(response) { 27 | return response.json(); 28 | } 29 | 30 | export function httpGet(url) { 31 | 32 | return fetch(url, { 33 | headers: buildHeaders(), 34 | }) 35 | .then(checkStatus) 36 | .then(parseJSON); 37 | } 38 | 39 | export function httpPost(url, data) { 40 | const body = JSON.stringify(data); 41 | 42 | return fetch(url, { 43 | method: 'post', 44 | headers: buildHeaders(), 45 | body: body, 46 | }) 47 | .then(checkStatus) 48 | .then(parseJSON); 49 | } 50 | 51 | export function httpDelete(url) { 52 | const authToken = localStorage.getItem('phoenixAuthToken'); 53 | 54 | return fetch(url, { 55 | method: 'delete', 56 | headers: buildHeaders(), 57 | }) 58 | .then(checkStatus) 59 | .then(parseJSON); 60 | } 61 | 62 | export function setDocumentTitle(title) { 63 | document.title = `${title} | HelloPhoenix`; 64 | } 65 | 66 | export function renderErrorsFor(errors, ref) { 67 | if (!errors) return false; 68 | 69 | return errors.map((error, i) => { 70 | if (error[ref]) { 71 | return ( 72 |
73 | {error[ref]} 74 |
75 | ); 76 | } 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /web/static/js/views/home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import classnames from 'classnames'; 4 | 5 | import { setDocumentTitle } from '../../utils'; 6 | 7 | class HomeIndexView extends React.Component { 8 | componentDidMount() { 9 | setDocumentTitle('Home'); 10 | } 11 | 12 | render() { 13 | return ( 14 |
15 | Hello, world! 16 |
17 | ); 18 | } 19 | } 20 | 21 | const mapStateToProps = (state) => ( 22 | state 23 | ); 24 | 25 | export default connect(mapStateToProps)(HomeIndexView); 26 | -------------------------------------------------------------------------------- /web/static/js/views/registrations/new.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | import { setDocumentTitle, renderErrorsFor } from '../../utils'; 6 | import Actions from '../../actions/registrations'; 7 | 8 | class RegistrationsNew extends React.Component { 9 | componentDidMount() { 10 | setDocumentTitle('Sign up'); 11 | } 12 | 13 | _handleSubmit(e) { 14 | e.preventDefault(); 15 | 16 | const { dispatch } = this.props; 17 | 18 | const data = { 19 | first_name: this.refs.firstName.value, 20 | email: this.refs.email.value, 21 | password: this.refs.password.value, 22 | }; 23 | 24 | dispatch(Actions.signUp(data)); 25 | } 26 | 27 | render() { 28 | const { errors } = this.props; 29 | 30 | return ( 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | {renderErrorsFor(errors, 'first_name')} 40 |
41 |
42 | 43 | {renderErrorsFor(errors, 'email')} 44 |
45 |
46 | 47 | {renderErrorsFor(errors, 'password')} 48 |
49 | 50 |
51 | Sign in 52 |
53 |
54 | ); 55 | } 56 | } 57 | 58 | const mapStateToProps = (state) => ({ 59 | errors: state.registration.errors, 60 | }); 61 | 62 | export default connect(mapStateToProps)(RegistrationsNew); 63 | -------------------------------------------------------------------------------- /web/static/js/views/sessions/new.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | import { setDocumentTitle } from '../../utils'; 6 | import Actions from '../../actions/sessions'; 7 | 8 | class SessionsNew extends React.Component { 9 | componentDidMount() { 10 | setDocumentTitle('Sign in'); 11 | } 12 | 13 | _handleSubmit(e) { 14 | e.preventDefault(); 15 | 16 | const { email, password } = this.refs; 17 | const { dispatch } = this.props; 18 | 19 | dispatch(Actions.signIn(email.value, password.value)); 20 | } 21 | 22 | _renderError() { 23 | let { error } = this.props; 24 | 25 | if (!error) return false; 26 | 27 | return ( 28 |
29 | {error} 30 |
31 | ); 32 | } 33 | 34 | render() { 35 | return ( 36 |
37 |
38 |
39 |
40 |
41 |
42 | {::this._renderError()} 43 |
44 | 50 |
51 |
52 | 58 |
59 | 60 |
61 | Create new account 62 |
63 |
64 | ); 65 | } 66 | } 67 | 68 | const mapStateToProps = (state) => ( 69 | state.session 70 | ); 71 | 72 | export default connect(mapStateToProps)(SessionsNew); 73 | -------------------------------------------------------------------------------- /web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Hello | HelloPhoenix 11 | "> 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 |
7 |
8 |

Resources

9 |
    10 |
  • 11 | Guides 12 |
  • 13 |
  • 14 | Docs 15 |
  • 16 |
  • 17 | Source 18 |
  • 19 |
20 |
21 | 22 |
23 |

Help

24 |
    25 |
  • 26 | Mailing list 27 |
  • 28 |
  • 29 | #elixir-lang on freenode IRC 30 |
  • 31 |
  • 32 | @elixirphoenix 33 |
  • 34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /web/views/current_user_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.CurrentUserView do 2 | use HelloPhoenix.Web, :view 3 | 4 | def render("show.json", %{user: user}) do 5 | user 6 | end 7 | 8 | def render("error.json", _) do 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.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 | if error = form.errors[field] do 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. On your own code and templates, 25 | # this could be written simply as: 26 | # 27 | # dngettext "errors", "1 file", "%{count} files", count 28 | # 29 | Gettext.dngettext(HelloPhoenix.Gettext, "errors", msg, msg, opts[:count], opts) 30 | end 31 | 32 | def translate_error(msg) do 33 | Gettext.dgettext(HelloPhoenix.Gettext, "errors", msg) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.ErrorView do 2 | use HelloPhoenix.Web, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Server internal 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 | -------------------------------------------------------------------------------- /web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.LayoutView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.PageView do 2 | use HelloPhoenix.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/registration_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.RegistrationView do 2 | use HelloPhoenix.Web, :view 3 | 4 | def render("error.json", %{changeset: changeset}) do 5 | errors = Enum.map(changeset.errors, fn {field, detail} -> 6 | %{} |> Map.put(field, render_detail(detail)) 7 | end) 8 | 9 | %{ 10 | errors: errors 11 | } 12 | end 13 | 14 | defp render_detail({message, values}) do 15 | Enum.reduce(values, message, fn {k, v}, acc -> String.replace(acc, "%{#{k}}", to_string(v)) end) 16 | end 17 | 18 | defp render_detail(message) do 19 | message 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /web/views/session_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.SessionView do 2 | use HelloPhoenix.Web, :view 3 | 4 | def render("show.json", %{jwt: jwt, user: user}) do 5 | %{ 6 | jwt: jwt, 7 | user: user 8 | } 9 | end 10 | 11 | def render("error.json", _) do 12 | %{error: "Invalid email or password"} 13 | end 14 | 15 | def render("delete.json", _) do 16 | %{ok: true} 17 | end 18 | 19 | def render("forbidden.json", %{error: error}) do 20 | %{error: error} 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /web/web.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloPhoenix.Web do 2 | @moduledoc """ 3 | A module that keeps using definitions for controllers, 4 | views and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use HelloPhoenix.Web, :controller 9 | use HelloPhoenix.Web, :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. 17 | """ 18 | 19 | def model do 20 | quote do 21 | use Ecto.Schema 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query, only: [from: 1, from: 2] 26 | end 27 | end 28 | 29 | def controller do 30 | quote do 31 | use Phoenix.Controller 32 | 33 | alias HelloPhoenix.Repo 34 | import Ecto 35 | import Ecto.Query, only: [from: 1, from: 2] 36 | 37 | import HelloPhoenix.Router.Helpers 38 | import HelloPhoenix.Gettext 39 | end 40 | end 41 | 42 | def view do 43 | quote do 44 | use Phoenix.View, root: "web/templates" 45 | 46 | # Import convenience functions from controllers 47 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] 48 | 49 | # Use all HTML functionality (forms, tags, etc) 50 | use Phoenix.HTML 51 | 52 | import HelloPhoenix.Router.Helpers 53 | import HelloPhoenix.ErrorHelpers 54 | import HelloPhoenix.Gettext 55 | end 56 | end 57 | 58 | def router do 59 | quote do 60 | use Phoenix.Router 61 | end 62 | end 63 | 64 | def channel do 65 | quote do 66 | use Phoenix.Channel 67 | 68 | alias HelloPhoenix.Repo 69 | import Ecto 70 | import Ecto.Query, only: [from: 1, from: 2] 71 | import HelloPhoenix.Gettext 72 | end 73 | end 74 | 75 | @doc """ 76 | When used, dispatch to the appropriate controller/view/etc. 77 | """ 78 | defmacro __using__(which) when is_atom(which) do 79 | apply(__MODULE__, which, []) 80 | end 81 | end 82 | --------------------------------------------------------------------------------