diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e8076f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# Mix artifacts
+/_build
+/deps
+/*.ez
+
+# Generate on crash by the VM
+erl_crash.dump
+
+# Static artifacts
+/node_modules
+
+# Since we are building js and css from web/static,
+# we ignore priv/static/{css,js}. You may want to
+# comment this depending on your deployment strategy.
+/priv/static/css
+/priv/static/js
+
+# The config/prod.secret.exs file by default contains sensitive
+# data and you should not commit it into version control.
+#
+# Alternatively, you may comment the line below and commit the
+# secrets file as long as you replace its contents by environment
+# variables.
+/config/prod.secret.exs
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f826a47
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2015 René Föhring
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fc16481
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+# ElixirStatus
+
+To start your new Phoenix application:
+
+1. Install dependencies with `mix deps.get`
+2. Start Phoenix endpoint with `mix phoenix.server`
+
+Now you can visit `localhost:4000` from your browser.
diff --git a/brunch-config.js b/brunch-config.js
new file mode 100644
index 0000000..691b7d1
--- /dev/null
+++ b/brunch-config.js
@@ -0,0 +1,46 @@
+exports.config = {
+ // See http://brunch.io/#documentation for docs.
+ files: {
+ javascripts: {
+ joinTo: 'js/app.js'
+ // To use a separate vendor.js bundle, specify two files path
+ // https://github.com/brunch/brunch/blob/stable/docs/config.md#files
+ // joinTo: {
+ // 'js/app.js': /^(web\/static\/js)/,
+ // 'js/vendor.js': /^(web\/static\/vendor)/
+ // }
+ //
+ // To change the order of concatenation of files, explictly mention here
+ // https://github.com/brunch/brunch/tree/stable/docs#concatenation
+ // order: {
+ // before: [
+ // 'web/static/vendor/js/jquery-2.1.1.js',
+ // 'web/static/vendor/js/bootstrap.min.js'
+ // ]
+ // }
+ },
+ stylesheets: {
+ joinTo: 'css/app.css'
+ },
+ templates: {
+ joinTo: 'js/app.js'
+ }
+ },
+
+ // Phoenix paths configuration
+ paths: {
+ // Which directories to watch
+ watched: ["web/static", "test/static"],
+
+ // Where to compile files to
+ public: "priv/static"
+ },
+
+ // Configure your plugins
+ plugins: {
+ babel: {
+ // Do not use ES6 compiler in vendor code
+ ignore: [/^(web\/static\/vendor)/]
+ }
+ }
+};
diff --git a/config/config.exs b/config/config.exs
new file mode 100644
index 0000000..9157625
--- /dev/null
+++ b/config/config.exs
@@ -0,0 +1,24 @@
+# This file is responsible for configuring your application
+# and its dependencies with the aid of the Mix.Config module.
+#
+# This configuration file is loaded before any dependency and
+# is restricted to this project.
+use Mix.Config
+
+# Configures the endpoint
+config :elixir_status, ElixirStatus.Endpoint,
+ url: [host: "localhost"],
+ root: Path.dirname(__DIR__),
+ secret_key_base: "LstH3/4lI4FeMXhEDifTp4UP+EAUR8RsyElC9rzzC7uyR1+hJ9U94iTHiajgzEwQ",
+ debug_errors: false,
+ pubsub: [name: ElixirStatus.PubSub,
+ adapter: Phoenix.PubSub.PG2]
+
+# Configures Elixir's Logger
+config :logger, :console,
+ format: "$time $metadata[$level] $message\n",
+ metadata: [:request_id]
+
+# Import environment specific config. This must remain at the bottom
+# of this file so it overrides the configuration defined above.
+import_config "#{Mix.env}.exs"
diff --git a/config/dev.exs b/config/dev.exs
new file mode 100644
index 0000000..9095b86
--- /dev/null
+++ b/config/dev.exs
@@ -0,0 +1,35 @@
+use Mix.Config
+
+# For development, we disable any cache and enable
+# debugging and code reloading.
+#
+# The watchers configuration can be used to run external
+# watchers to your application. For example, we use it
+# with brunch.io to recompile .js and .css sources.
+config :elixir_status, ElixirStatus.Endpoint,
+ http: [port: 4000],
+ debug_errors: true,
+ code_reloader: true,
+ cache_static_lookup: false,
+ watchers: [node: ["node_modules/brunch/bin/brunch", "watch"]]
+
+# Watch static and templates for browser reloading.
+config :elixir_status, ElixirStatus.Endpoint,
+ live_reload: [
+ patterns: [
+ ~r{priv/static/.*(js|css|png|jpeg|jpg|gif)$},
+ ~r{web/views/.*(ex)$},
+ ~r{web/templates/.*(eex)$}
+ ]
+ ]
+
+# Do not include metadata nor timestamps in development logs
+config :logger, :console, format: "[$level] $message\n"
+
+# Configure your database
+config :elixir_status, ElixirStatus.Repo,
+ adapter: Ecto.Adapters.Postgres,
+ username: "postgres",
+ password: "postgres",
+ database: "elixir_status_dev",
+ size: 10 # The amount of database connections in the pool
diff --git a/config/prod.exs b/config/prod.exs
new file mode 100644
index 0000000..dc3a61d
--- /dev/null
+++ b/config/prod.exs
@@ -0,0 +1,51 @@
+use Mix.Config
+
+# For production, we configure the host to read the PORT
+# from the system environment. Therefore, you will need
+# to set PORT=80 before running your server.
+#
+# You should also configure the url host to something
+# meaningful, we use this information when generating URLs.
+#
+# Finally, we also include the path to a manifest
+# containing the digested version of static files. This
+# manifest is generated by the mix phoenix.digest task
+# which you typically run after static files are built.
+config :elixir_status, ElixirStatus.Endpoint,
+ http: [port: {:system, "PORT"}],
+ url: [host: "example.com"],
+ cache_static_manifest: "priv/static/manifest.json"
+
+# ## SSL Support
+#
+# To get SSL working, you will need to add the `https` key
+# to the previous section:
+#
+# config :elixir_status, ElixirStatus.Endpoint,
+# ...
+# https: [port: 443,
+# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
+#
+# Where those two env variables point to a file on
+# disk for the key and cert.
+
+# Do not print debug messages in production
+config :logger, level: :info
+
+# ## Using releases
+#
+# If you are doing OTP releases, you need to instruct Phoenix
+# to start the server for all endpoints:
+#
+# config :phoenix, :serve_endpoints, true
+#
+# Alternatively, you can configure exactly which server to
+# start per endpoint:
+#
+# config :elixir_status, ElixirStatus.Endpoint, server: true
+#
+
+# Finally import the config/prod.secret.exs
+# which should be versioned separately.
+import_config "prod.secret.exs"
diff --git a/config/test.exs b/config/test.exs
new file mode 100644
index 0000000..f052d8e
--- /dev/null
+++ b/config/test.exs
@@ -0,0 +1,18 @@
+use Mix.Config
+
+# We don't run a server during test. If one is required,
+# you can enable the server option below.
+config :elixir_status, ElixirStatus.Endpoint,
+ http: [port: 4001],
+ server: false
+
+# Print only warnings and errors during test
+config :logger, level: :warn
+
+# Configure your database
+config :elixir_status, ElixirStatus.Repo,
+ adapter: Ecto.Adapters.Postgres,
+ username: "postgres",
+ password: "postgres",
+ database: "elixir_status_test",
+ size: 1 # Use a single connection for transactional tests
diff --git a/lib/elixir_status.ex b/lib/elixir_status.ex
new file mode 100644
index 0000000..e344bd2
--- /dev/null
+++ b/lib/elixir_status.ex
@@ -0,0 +1,30 @@
+defmodule ElixirStatus do
+ use Application
+
+ # See http://elixir-lang.org/docs/stable/elixir/Application.html
+ # for more information on OTP Applications
+ def start(_type, _args) do
+ import Supervisor.Spec, warn: false
+
+ children = [
+ # Start the endpoint when the application starts
+ supervisor(ElixirStatus.Endpoint, []),
+ # Start the Ecto repository
+ worker(ElixirStatus.Repo, []),
+ # Here you could define other workers and supervisors as children
+ # worker(ElixirStatus.Worker, [arg1, arg2, arg3]),
+ ]
+
+ # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: ElixirStatus.Supervisor]
+ Supervisor.start_link(children, opts)
+ end
+
+ # Tell Phoenix to update the endpoint configuration
+ # whenever the application is updated.
+ def config_change(changed, _new, removed) do
+ ElixirStatus.Endpoint.config_change(changed, removed)
+ :ok
+ end
+end
diff --git a/lib/elixir_status/endpoint.ex b/lib/elixir_status/endpoint.ex
new file mode 100644
index 0000000..dfe9494
--- /dev/null
+++ b/lib/elixir_status/endpoint.ex
@@ -0,0 +1,35 @@
+defmodule ElixirStatus.Endpoint do
+ use Phoenix.Endpoint, otp_app: :elixir_status
+
+ # Serve at "/" the static files from "priv/static" directory.
+ #
+ # You should set gzip to true if you are running phoenix.digest
+ # when deploying your static files in production.
+ plug Plug.Static,
+ at: "/", from: :elixir_status, gzip: false,
+ only: ~w(css images js favicon.ico robots.txt)
+
+ # Code reloading can be explicitly enabled under the
+ # :code_reloader configuration of your endpoint.
+ if code_reloading? do
+ plug Phoenix.LiveReloader
+ plug Phoenix.CodeReloader
+ end
+
+ plug Plug.Logger
+
+ plug Plug.Parsers,
+ parsers: [:urlencoded, :multipart, :json],
+ pass: ["*/*"],
+ json_decoder: Poison
+
+ plug Plug.MethodOverride
+ plug Plug.Head
+
+ plug Plug.Session,
+ store: :cookie,
+ key: "_elixir_status_key",
+ signing_salt: "XPXyl9lg"
+
+ plug :router, ElixirStatus.Router
+end
diff --git a/lib/elixir_status/repo.ex b/lib/elixir_status/repo.ex
new file mode 100644
index 0000000..7e98fac
--- /dev/null
+++ b/lib/elixir_status/repo.ex
@@ -0,0 +1,3 @@
+defmodule ElixirStatus.Repo do
+ use Ecto.Repo, otp_app: :elixir_status
+end
diff --git a/mix.exs b/mix.exs
new file mode 100644
index 0000000..60d45a2
--- /dev/null
+++ b/mix.exs
@@ -0,0 +1,40 @@
+defmodule ElixirStatus.Mixfile do
+ use Mix.Project
+
+ def project do
+ [app: :elixir_status,
+ version: "0.0.1",
+ elixir: "~> 1.0",
+ elixirc_paths: elixirc_paths(Mix.env),
+ compilers: [:phoenix] ++ Mix.compilers,
+ build_embedded: Mix.env == :prod,
+ start_permanent: Mix.env == :prod,
+ deps: deps]
+ end
+
+ # Configuration for the OTP application
+ #
+ # Type `mix help compile.app` for more information
+ def application do
+ [mod: {ElixirStatus, []},
+ applications: [:phoenix, :phoenix_html, :cowboy, :logger,
+ :phoenix_ecto, :postgrex, :oauth2]]
+ end
+
+ # Specifies which paths to compile per environment
+ defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
+ defp elixirc_paths(_), do: ["lib", "web"]
+
+ # Specifies your project dependencies
+ #
+ # Type `mix help deps` for examples and options
+ defp deps do
+ [{:phoenix, "~> 0.13.1"},
+ {:phoenix_ecto, "~> 0.4"},
+ {:postgrex, ">= 0.0.0"},
+ {:phoenix_html, "~> 1.0"},
+ {:phoenix_live_reload, "~> 0.4", only: :dev},
+ {:cowboy, "~> 1.0"},
+ {:oauth2, "~> 0.1.0"}]
+ end
+end
diff --git a/mix.lock b/mix.lock
new file mode 100644
index 0000000..5bf3a3b
--- /dev/null
+++ b/mix.lock
@@ -0,0 +1,19 @@
+%{"cowboy": {:hex, :cowboy, "1.0.0"},
+ "cowlib": {:hex, :cowlib, "1.0.1"},
+ "decimal": {:hex, :decimal, "1.1.0"},
+ "ecto": {:hex, :ecto, "0.12.0-rc"},
+ "fs": {:hex, :fs, "0.9.2"},
+ "hackney": {:hex, :hackney, "1.1.0"},
+ "httpoison": {:hex, :httpoison, "0.7.0"},
+ "idna": {:hex, :idna, "1.0.2"},
+ "oauth2": {:hex, :oauth2, "0.1.1"},
+ "phoenix": {:hex, :phoenix, "0.13.1"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "0.4.0"},
+ "phoenix_html": {:hex, :phoenix_html, "1.2.1"},
+ "phoenix_live_reload": {:hex, :phoenix_live_reload, "0.4.1"},
+ "plug": {:hex, :plug, "0.13.0"},
+ "poison": {:hex, :poison, "1.4.0"},
+ "poolboy": {:hex, :poolboy, "1.5.1"},
+ "postgrex": {:hex, :postgrex, "0.8.2"},
+ "ranch": {:hex, :ranch, "1.0.0"},
+ "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"}}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..dc440c9
--- /dev/null
+++ b/package.json
@@ -0,0 +1,13 @@
+{
+ "repository": {
+ },
+ "dependencies": {
+ "brunch": "^1.8.1",
+ "babel-brunch": "^5.1.1",
+ "clean-css-brunch": ">= 1.0 < 1.8",
+ "css-brunch": ">= 1.0 < 1.8",
+ "javascript-brunch": ">= 1.0 < 1.8",
+ "sass-brunch": "^1.8.10",
+ "uglify-js-brunch": ">= 1.0 < 1.8"
+ }
+}
diff --git a/priv/static/images/avatar_rrrene.jpg b/priv/static/images/avatar_rrrene.jpg
new file mode 100755
index 0000000..63908d2
Binary files /dev/null and b/priv/static/images/avatar_rrrene.jpg differ
diff --git a/priv/static/images/phoenix.png b/priv/static/images/phoenix.png
new file mode 100644
index 0000000..9c81075
Binary files /dev/null and b/priv/static/images/phoenix.png differ
diff --git a/priv/static/robots.txt b/priv/static/robots.txt
new file mode 100644
index 0000000..3c9c7c0
--- /dev/null
+++ b/priv/static/robots.txt
@@ -0,0 +1,5 @@
+# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-agent: *
+# Disallow: /
diff --git a/test/controllers/page_controller_test.exs b/test/controllers/page_controller_test.exs
new file mode 100644
index 0000000..ce83bae
--- /dev/null
+++ b/test/controllers/page_controller_test.exs
@@ -0,0 +1,8 @@
+defmodule ElixirStatus.PageControllerTest do
+ use ElixirStatus.ConnCase
+
+ test "GET /" do
+ conn = get conn(), "/"
+ assert html_response(conn, 200) =~ "Welcome to Phoenix!"
+ end
+end
diff --git a/test/support/channel_case.ex b/test/support/channel_case.ex
new file mode 100644
index 0000000..8ff4c3c
--- /dev/null
+++ b/test/support/channel_case.ex
@@ -0,0 +1,41 @@
+defmodule ElixirStatus.ChannelCase do
+ @moduledoc """
+ This module defines the test case to be used by
+ channel tests.
+
+ Such tests rely on `Phoenix.ChannelTest` and also
+ imports other functionality to make it easier
+ to build and query models.
+
+ Finally, if the test case interacts with the database,
+ it cannot be async. For this reason, every test runs
+ inside a transaction which is reset at the beginning
+ of the test unless the test case is marked as async.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ # Import conveniences for testing with channels
+ use Phoenix.ChannelTest
+
+ # Alias the data repository and import query/model functions
+ alias ElixirStatus.Repo
+ import Ecto.Model
+ import Ecto.Query, only: [from: 2]
+
+
+ # The default endpoint for testing
+ @endpoint ElixirStatus.Endpoint
+ end
+ end
+
+ setup tags do
+ unless tags[:async] do
+ Ecto.Adapters.SQL.restart_test_transaction(ElixirStatus.Repo, [])
+ end
+
+ :ok
+ end
+end
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
new file mode 100644
index 0000000..7ec0178
--- /dev/null
+++ b/test/support/conn_case.ex
@@ -0,0 +1,43 @@
+defmodule ElixirStatus.ConnCase do
+ @moduledoc """
+ This module defines the test case to be used by
+ tests that require setting up a connection.
+
+ Such tests rely on `Phoenix.ConnTest` and also
+ imports other functionality to make it easier
+ to build and query models.
+
+ Finally, if the test case interacts with the database,
+ it cannot be async. For this reason, every test runs
+ inside a transaction which is reset at the beginning
+ of the test unless the test case is marked as async.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ # Import conveniences for testing with connections
+ use Phoenix.ConnTest
+
+ # Alias the data repository and import query/model functions
+ alias ElixirStatus.Repo
+ import Ecto.Model
+ import Ecto.Query, only: [from: 2]
+
+ # Import URL helpers from the router
+ import ElixirStatus.Router.Helpers
+
+ # The default endpoint for testing
+ @endpoint ElixirStatus.Endpoint
+ end
+ end
+
+ setup tags do
+ unless tags[:async] do
+ Ecto.Adapters.SQL.restart_test_transaction(ElixirStatus.Repo, [])
+ end
+
+ :ok
+ end
+end
diff --git a/test/support/model_case.ex b/test/support/model_case.ex
new file mode 100644
index 0000000..b5f044b
--- /dev/null
+++ b/test/support/model_case.ex
@@ -0,0 +1,53 @@
+defmodule ElixirStatus.ModelCase do
+ @moduledoc """
+ This module defines the test case to be used by
+ model tests.
+
+ You may define functions here to be used as helpers in
+ your model tests. See `errors_on/2`'s definition as reference.
+
+ Finally, if the test case interacts with the database,
+ it cannot be async. For this reason, every test runs
+ inside a transaction which is reset at the beginning
+ of the test unless the test case is marked as async.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ # Alias the data repository and import query/model functions
+ alias ElixirStatus.Repo
+ import Ecto.Model
+ import Ecto.Query, only: [from: 2]
+ import ElixirStatus.ModelCase
+ end
+ end
+
+ setup tags do
+ unless tags[:async] do
+ Ecto.Adapters.SQL.restart_test_transaction(ElixirStatus.Repo, [])
+ end
+
+ :ok
+ end
+
+ @doc """
+ Helper for returning list of errors in model when passed certain data.
+
+ ## Examples
+
+ Given a User model that has validation for the presence of a value for the
+ `:name` field and validation that `:password` is "safe":
+
+ iex> errors_on(%User{}, password: "password")
+ [{:password, "is unsafe"}, {:name, "is blank"}]
+
+ You would then write your assertion like:
+
+ assert {:password, "is unsafe"} in errors_on(%User{}, password: "password")
+ """
+ def errors_on(model, data) do
+ model.__struct__.changeset(model, data).errors
+ end
+end
diff --git a/test/test_helper.exs b/test/test_helper.exs
new file mode 100644
index 0000000..a892c10
--- /dev/null
+++ b/test/test_helper.exs
@@ -0,0 +1,6 @@
+ExUnit.start
+
+# Create the database, run migrations, and start the test transaction.
+Mix.Task.run "ecto.create", ["--quiet"]
+Mix.Task.run "ecto.migrate", ["--quiet"]
+Ecto.Adapters.SQL.begin_test_transaction(ElixirStatus.Repo)
diff --git a/test/views/error_view_test.exs b/test/views/error_view_test.exs
new file mode 100644
index 0000000..ee9e23b
--- /dev/null
+++ b/test/views/error_view_test.exs
@@ -0,0 +1,21 @@
+defmodule ElixirStatus.ErrorViewTest do
+ use ElixirStatus.ConnCase, async: true
+
+ # Bring render/3 and render_to_string/3 for testing custom views
+ import Phoenix.View
+
+ test "renders 404.html" do
+ assert render_to_string(ElixirStatus.ErrorView, "404.html", []) ==
+ "Page not found"
+ end
+
+ test "render 500.html" do
+ assert render_to_string(ElixirStatus.ErrorView, "500.html", []) ==
+ "Server internal error"
+ end
+
+ test "render any other" do
+ assert render_to_string(ElixirStatus.ErrorView, "505.html", []) ==
+ "Server internal error"
+ end
+end
diff --git a/test/views/page_view_test.exs b/test/views/page_view_test.exs
new file mode 100644
index 0000000..f804105
--- /dev/null
+++ b/test/views/page_view_test.exs
@@ -0,0 +1,3 @@
+defmodule ElixirStatus.PageViewTest do
+ use ElixirStatus.ConnCase, async: true
+end
diff --git a/web/controllers/github_auth_controller.ex b/web/controllers/github_auth_controller.ex
new file mode 100644
index 0000000..6e12f9b
--- /dev/null
+++ b/web/controllers/github_auth_controller.ex
@@ -0,0 +1,51 @@
+defmodule ElixirStatus.GitHubAuthController do
+ use ElixirStatus.Web, :controller
+
+ plug :action
+
+ @doc """
+ This action is reached via `/auth` and redirects to the OAuth2 provider
+ based on the chosen strategy.
+ """
+ def sign_in(conn, _params) do
+ redirect conn, external: GitHubAuth.authorize_url!
+ end
+
+ @doc """
+ This action is reached via `/auth/sign_out` and redirects to the OAuth2 provider
+ based on the chosen strategy.
+ """
+ def sign_out(conn, _params) do
+ conn
+ |> put_session(:current_user, nil)
+ |> put_session(:access_token, nil)
+ |> redirect(to: "/")
+ end
+
+ @doc """
+ This action is reached via `/auth/callback` is the the callback URL that
+ the OAuth2 provider will redirect the user back to with a `code` that will
+ be used to request an access token. The access token will then be used to
+ access protected resources on behalf of the user.
+ """
+ def callback(conn, %{"code" => code}) do
+ # Exchange an auth code for an access token
+ token = GitHubAuth.get_token!(code: code)
+
+ # Request the user's data with the access token
+ user = OAuth2.AccessToken.get!(token, "/user")
+
+ # Store the user in the session under `:current_user` and redirect to /.
+ # In most cases, we'd probably just store the user's ID that can be used
+ # to fetch from the database. In this case, since this example app has no
+ # database, I'm just storing the user map.
+ #
+ # If you need to make additional resource requests, you may want to store
+ # the access token as well.
+ conn
+ |> put_session(:current_user, user)
+ |> put_session(:access_token, token.access_token)
+ |> redirect(to: "/")
+ end
+end
+
diff --git a/web/controllers/page_controller.ex b/web/controllers/page_controller.ex
new file mode 100644
index 0000000..073b1cc
--- /dev/null
+++ b/web/controllers/page_controller.ex
@@ -0,0 +1,9 @@
+defmodule ElixirStatus.PageController do
+ use ElixirStatus.Web, :controller
+
+ plug :action
+
+ def index(conn, _params) do
+ render conn, "index.html"
+ end
+end
diff --git a/web/models/user.ex b/web/models/user.ex
new file mode 100644
index 0000000..c39b377
--- /dev/null
+++ b/web/models/user.ex
@@ -0,0 +1,9 @@
+defmodule ElixirStatus.User do
+ @moduledoc """
+ The `current_user` is stored in the connection as a map
+ with string keys.
+ """
+
+ def name(user), do: user["name"]
+ def email(user), do: user["email"]
+end
diff --git a/web/oauth/git_hub_auth.ex b/web/oauth/git_hub_auth.ex
new file mode 100644
index 0000000..32dd523
--- /dev/null
+++ b/web/oauth/git_hub_auth.ex
@@ -0,0 +1,42 @@
+defmodule GitHubAuth do
+ @moduledoc """
+ An OAuth2 strategy for GitHub.
+ """
+ use OAuth2.Strategy
+
+ alias OAuth2.Strategy.AuthCode
+
+ # Public API
+
+ def new do
+ OAuth2.new([
+ strategy: __MODULE__,
+ client_id: System.get_env("CLIENT_ID"),
+ client_secret: System.get_env("CLIENT_SECRET"),
+ redirect_uri: System.get_env("REDIRECT_URI"),
+ site: "https://api.github.com",
+ authorize_url: "https://github.com/login/oauth/authorize",
+ token_url: "https://github.com/login/oauth/access_token"
+ ])
+ end
+
+ def authorize_url!(params \\ []) do
+ OAuth2.Client.authorize_url!(new(), params)
+ end
+
+ def get_token!(params \\ [], headers \\ []) do
+ OAuth2.Client.get_token!(new(), params)
+ end
+
+ # Strategy Callbacks
+
+ def authorize_url(client, params) do
+ AuthCode.authorize_url(client, params)
+ end
+
+ def get_token(client, params, headers) do
+ client
+ |> put_header("Accept", "application/json")
+ |> AuthCode.get_token(params, headers)
+ end
+end
diff --git a/web/router.ex b/web/router.ex
new file mode 100644
index 0000000..c099fe4
--- /dev/null
+++ b/web/router.ex
@@ -0,0 +1,40 @@
+defmodule ElixirStatus.Router do
+ use ElixirStatus.Web, :router
+
+ pipeline :browser do
+ plug :accepts, ["html"]
+ plug :fetch_session
+ plug :fetch_flash
+ plug :protect_from_forgery
+ plug :assign_current_user
+ end
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/", ElixirStatus do
+ pipe_through :browser # Use the default browser stack
+
+ get "/", PageController, :index
+ end
+
+ scope "/auth", alias: ElixirStatus do
+ pipe_through :browser
+ get "/", GitHubAuthController, :sign_in
+ get "/sign_out", GitHubAuthController, :sign_out
+ get "/callback", GitHubAuthController, :callback
+ end
+
+ # Fetch the current user from the session and add it to `conn.assigns`. This
+ # will allow you to have access to the current user in your views with
+ # `@current_user`.
+ defp assign_current_user(conn, _) do
+ assign(conn, :current_user, get_session(conn, :current_user))
+ end
+
+ # Other scopes may use custom stacks.
+ # scope "/api", ElixirStatus do
+ # pipe_through :api
+ # end
+end
diff --git a/web/static/css/1_poole.css b/web/static/css/1_poole.css
new file mode 100644
index 0000000..5d137f6
--- /dev/null
+++ b/web/static/css/1_poole.css
@@ -0,0 +1,427 @@
+/*
+ * ___
+ * /\_ \
+ * _____ ___ ___\//\ \ __
+ * /\ '__`\ / __`\ / __`\\ \ \ /'__`\
+ * \ \ \_\ \/\ \_\ \/\ \_\ \\_\ \_/\ __/
+ * \ \ ,__/\ \____/\ \____//\____\ \____\
+ * \ \ \/ \/___/ \/___/ \/____/\/____/
+ * \ \_\
+ * \/_/
+ *
+ * Designed, built, and released under MIT license by @mdo. Learn more at
+ * https://github.com/poole/poole.
+ */
+
+
+/*
+ * Contents
+ *
+ * Body resets
+ * Custom type
+ * Messages
+ * Container
+ * Masthead
+ * Posts and pages
+ * Pagination
+ * Reverse layout
+ * Themes
+ */
+
+
+/*
+ * Body resets
+ *
+ * Update the foundational and global aspects of the page.
+ */
+
+* {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 1.5;
+}
+@media (min-width: 38em) {
+ html {
+ font-size: 20px;
+ }
+}
+
+body {
+ color: #515151;
+ background-color: #fff;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+}
+
+/* No `:visited` state is required by default (browsers will use `a`) */
+a {
+ color: #268bd2;
+ text-decoration: none;
+}
+a strong {
+ color: inherit;
+}
+/* `:focus` is linked to `:hover` for basic accessibility */
+a:hover,
+a:focus {
+ text-decoration: underline;
+}
+
+/* Headings */
+h1, h2, h3, h4, h5, h6 {
+ margin-bottom: .5rem;
+ font-weight: bold;
+ line-height: 1.25;
+ color: #313131;
+ text-rendering: optimizeLegibility;
+}
+h1 {
+ margin-top: 1rem;
+ font-size: 1.5rem;
+}
+h2 {
+ margin-top: 1.5rem;
+ font-size: 1.25rem;
+}
+h3, h4, h5, h6 {
+ margin-top: 1rem;
+ font-size: 1rem;
+}
+
+/* Body text */
+p {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+strong {
+ color: #303030;
+}
+
+
+/* Lists */
+ul, ol, dl {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+dt {
+ font-weight: bold;
+}
+dd {
+ margin-bottom: .5rem;
+}
+
+/* Misc */
+hr {
+ position: relative;
+ margin: 1.5rem 0;
+ border: 0;
+ border-top: 1px solid #eee;
+ border-bottom: 1px solid #fff;
+}
+
+abbr {
+ font-size: 85%;
+ font-weight: bold;
+ color: #555;
+ text-transform: uppercase;
+}
+abbr[title] {
+ cursor: help;
+ border-bottom: 1px dotted #e5e5e5;
+}
+
+/* Code */
+code,
+pre {
+ font-family: Menlo, Monaco, "Courier New", monospace;
+}
+code {
+ padding: .25em .5em;
+ font-size: 85%;
+ color: #bf616a;
+ background-color: #f9f9f9;
+ border-radius: 3px;
+}
+pre {
+ display: block;
+ margin-top: 0;
+ margin-bottom: 1rem;
+ padding: 1rem;
+ font-size: .8rem;
+ line-height: 1.4;
+ white-space: pre;
+ white-space: pre-wrap;
+ word-break: break-all;
+ word-wrap: break-word;
+ background-color: #f9f9f9;
+}
+pre code {
+ padding: 0;
+ font-size: 100%;
+ color: inherit;
+ background-color: transparent;
+}
+
+/* Pygments via Jekyll */
+.highlight {
+ margin-bottom: 1rem;
+ border-radius: 4px;
+}
+.highlight pre {
+ margin-bottom: 0;
+}
+
+/* Gist via GitHub Pages */
+.gist .gist-file {
+ font-family: Menlo, Monaco, "Courier New", monospace !important;
+}
+.gist .markdown-body {
+ padding: 15px;
+}
+.gist pre {
+ padding: 0;
+ background-color: transparent;
+}
+.gist .gist-file .gist-data {
+ font-size: .8rem !important;
+ line-height: 1.4;
+}
+.gist code {
+ padding: 0;
+ color: inherit;
+ background-color: transparent;
+ border-radius: 0;
+}
+
+/* Quotes */
+blockquote {
+ padding: .5rem 1rem;
+ margin: .8rem 0;
+ color: #7a7a7a;
+ border-left: .25rem solid #e5e5e5;
+}
+blockquote p:last-child {
+ margin-bottom: 0;
+}
+@media (min-width: 30em) {
+ blockquote {
+ padding-right: 4rem;
+ padding-left: 1.25rem;
+ }
+}
+
+img {
+ display: block;
+ max-width: 100%;
+ margin: 0 0 1rem;
+ border-radius: 5px;
+}
+
+/* Tables */
+table {
+ margin-bottom: 1rem;
+ width: 100%;
+ border: 1px solid #e5e5e5;
+ border-collapse: collapse;
+}
+td,
+th {
+ padding: .25rem .5rem;
+ border: 1px solid #e5e5e5;
+}
+tbody tr:nth-child(odd) td,
+tbody tr:nth-child(odd) th {
+ background-color: #f9f9f9;
+}
+
+
+/*
+ * Custom type
+ *
+ * Extend paragraphs with `.lead` for larger introductory text.
+ */
+
+.lead {
+ font-size: 1.25rem;
+ font-weight: 300;
+}
+
+
+/*
+ * Messages
+ *
+ * Show alert messages to users. You may add it to single elements like a `
`,
+ * or to a parent if there are multiple elements to show.
+ */
+
+.message {
+ margin-bottom: 1rem;
+ padding: 1rem;
+ color: #717171;
+ background-color: #f9f9f9;
+}
+
+
+/*
+ * Container
+ *
+ * Center the page content.
+ */
+
+.container {
+ max-width: 38rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+
+/*
+ * Masthead
+ *
+ * Super small header above the content for site name and short description.
+ */
+
+.masthead {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+ margin-bottom: 3rem;
+}
+.masthead-title {
+ margin-top: 0;
+ margin-bottom: 0;
+ color: #505050;
+}
+.masthead-title a {
+ color: #505050;
+}
+.masthead-title small {
+ font-size: 75%;
+ font-weight: 400;
+ color: #c0c0c0;
+ letter-spacing: 0;
+}
+
+
+/*
+ * Posts and pages
+ *
+ * Each post is wrapped in `.post` and is used on default and post layouts. Each
+ * page is wrapped in `.page` and is only used on the page layout.
+ */
+
+.page,
+.post {
+ margin-bottom: 2em;
+}
+
+/* Blog post or page title */
+.page-title,
+.post-title,
+.post-title a {
+ color: #303030;
+}
+.page-title,
+.post-title {
+ margin-top: 0;
+}
+
+/* Meta data line below post title */
+.post-date {
+ display: block;
+ margin-top: -.5rem;
+ margin-bottom: 1rem;
+ color: #9a9a9a;
+}
+
+/* Related posts */
+.related {
+ padding-top: 2rem;
+ padding-bottom: 2rem;
+ border-top: 1px solid #eee;
+}
+.related-posts {
+ padding-left: 0;
+ list-style: none;
+}
+.related-posts h3 {
+ margin-top: 0;
+}
+.related-posts li small {
+ font-size: 75%;
+ color: #999;
+}
+.related-posts li a:hover {
+ color: #268bd2;
+ text-decoration: none;
+}
+.related-posts li a:hover small {
+ color: inherit;
+}
+
+
+/*
+ * Pagination
+ *
+ * Super lightweight (HTML-wise) blog pagination. `span`s are provide for when
+ * there are no more previous or next posts to show.
+ */
+
+.pagination {
+ overflow: hidden; /* clearfix */
+ margin-left: -1rem;
+ margin-right: -1rem;
+ font-family: "PT Sans", Helvetica, Arial, sans-serif;
+ color: #ccc;
+ text-align: center;
+}
+
+/* Pagination items can be `span`s or `a`s */
+.pagination-item {
+ display: block;
+ padding: 1rem;
+ border: 1px solid #eee;
+}
+.pagination-item:first-child {
+ margin-bottom: -1px;
+}
+
+/* Only provide a hover state for linked pagination items */
+a.pagination-item:hover {
+ background-color: #f5f5f5;
+}
+
+@media (min-width: 30em) {
+ .pagination {
+ margin: 3rem 0;
+ }
+ .pagination-item {
+ float: left;
+ width: 50%;
+ }
+ .pagination-item:first-child {
+ margin-bottom: 0;
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ }
+ .pagination-item:last-child {
+ margin-left: -1px;
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ }
+}
\ No newline at end of file
diff --git a/web/static/css/3_syntax.css b/web/static/css/3_syntax.css
new file mode 100644
index 0000000..1264b87
--- /dev/null
+++ b/web/static/css/3_syntax.css
@@ -0,0 +1,66 @@
+.hll { background-color: #ffffcc }
+ /*{ background: #f0f3f3; }*/
+.c { color: #999; } /* Comment */
+.err { color: #AA0000; background-color: #FFAAAA } /* Error */
+.k { color: #006699; } /* Keyword */
+.o { color: #555555 } /* Operator */
+.cm { color: #0099FF; font-style: italic } /* Comment.Multiline */
+.cp { color: #009999 } /* Comment.Preproc */
+.c1 { color: #999; } /* Comment.Single */
+.cs { color: #999; } /* Comment.Special */
+.gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */
+.ge { font-style: italic } /* Generic.Emph */
+.gr { color: #FF0000 } /* Generic.Error */
+.gh { color: #003300; } /* Generic.Heading */
+.gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */
+.go { color: #AAAAAA } /* Generic.Output */
+.gp { color: #000099; } /* Generic.Prompt */
+.gs { } /* Generic.Strong */
+.gu { color: #003300; } /* Generic.Subheading */
+.gt { color: #99CC66 } /* Generic.Traceback */
+.kc { color: #006699; } /* Keyword.Constant */
+.kd { color: #006699; } /* Keyword.Declaration */
+.kn { color: #006699; } /* Keyword.Namespace */
+.kp { color: #006699 } /* Keyword.Pseudo */
+.kr { color: #006699; } /* Keyword.Reserved */
+.kt { color: #007788; } /* Keyword.Type */
+.m { color: #FF6600 } /* Literal.Number */
+.s { color: #d44950 } /* Literal.String */
+.na { color: #4f9fcf } /* Name.Attribute */
+.nb { color: #336666 } /* Name.Builtin */
+.nc { color: #00AA88; } /* Name.Class */
+.no { color: #336600 } /* Name.Constant */
+.nd { color: #9999FF } /* Name.Decorator */
+.ni { color: #999999; } /* Name.Entity */
+.ne { color: #CC0000; } /* Name.Exception */
+.nf { color: #CC00FF } /* Name.Function */
+.nl { color: #9999FF } /* Name.Label */
+.nn { color: #00CCFF; } /* Name.Namespace */
+.nt { color: #2f6f9f; } /* Name.Tag */
+.nv { color: #003333 } /* Name.Variable */
+.ow { color: #000000; } /* Operator.Word */
+.w { color: #bbbbbb } /* Text.Whitespace */
+.mf { color: #FF6600 } /* Literal.Number.Float */
+.mh { color: #FF6600 } /* Literal.Number.Hex */
+.mi { color: #FF6600 } /* Literal.Number.Integer */
+.mo { color: #FF6600 } /* Literal.Number.Oct */
+.sb { color: #CC3300 } /* Literal.String.Backtick */
+.sc { color: #CC3300 } /* Literal.String.Char */
+.sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */
+.s2 { color: #CC3300 } /* Literal.String.Double */
+.se { color: #CC3300; } /* Literal.String.Escape */
+.sh { color: #CC3300 } /* Literal.String.Heredoc */
+.si { color: #AA0000 } /* Literal.String.Interpol */
+.sx { color: #CC3300 } /* Literal.String.Other */
+.sr { color: #33AAAA } /* Literal.String.Regex */
+.s1 { color: #CC3300 } /* Literal.String.Single */
+.ss { color: #FFCC33 } /* Literal.String.Symbol */
+.bp { color: #336666 } /* Name.Builtin.Pseudo */
+.vc { color: #003333 } /* Name.Variable.Class */
+.vg { color: #003333 } /* Name.Variable.Global */
+.vi { color: #003333 } /* Name.Variable.Instance */
+.il { color: #FF6600 } /* Literal.Number.Integer.Long */
+
+.css .o,
+.css .o + .nt,
+.css .nt + .nt { color: #999; }
diff --git a/web/static/css/app.scss b/web/static/css/app.scss
new file mode 100644
index 0000000..e946c33
--- /dev/null
+++ b/web/static/css/app.scss
@@ -0,0 +1,57 @@
+.post-author {
+ position: absolute;
+ margin-left: -80px;
+
+ .avatar {
+ background-size: 100% 100%;
+ width: 64px;
+ height: 64px;
+ border-radius: 32px;
+ }
+}
+
+footer,
+footer a {
+ color: #aaa;
+ font-size: 0.8rem;
+}
+
+
+.btn-primary {
+ color: #fff;
+ background-color: #428bca;
+ border-color: #357ebd;
+}
+.btn {
+ display: inline-block;
+ margin-bottom: 0;
+ font-weight: 400;
+ text-align: center;
+ vertical-align: middle;
+ cursor: pointer;
+ background-image: none;
+ border: 1px solid transparent;
+ white-space: nowrap;
+ padding: 6px 12px;
+ font-size: 14px;
+ line-height: 1.42857143;
+ border-radius: 4px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.btn {
+ padding: 10px 16px;
+ font-size: 18px;
+ line-height: 1.33;
+ border-radius: 6px;
+}
+.btn-primary:hover {
+ color: #fff;
+ background-color: #286090;
+ border-color: #204d74;
+}
+.btn.focus, .btn:focus, .btn:hover {
+ text-decoration: none;
+}
\ No newline at end of file
diff --git a/web/static/js/app.js b/web/static/js/app.js
new file mode 100644
index 0000000..f88a60a
--- /dev/null
+++ b/web/static/js/app.js
@@ -0,0 +1,13 @@
+import {Socket} from "phoenix"
+
+// let socket = new Socket("/ws")
+// socket.connect()
+// let chan = socket.chan("topic:subtopic", {})
+// chan.join().receive("ok", chan => {
+// console.log("Success!")
+// })
+
+let App = {
+}
+
+export default App
diff --git a/web/static/vendor/phoenix.js b/web/static/vendor/phoenix.js
new file mode 100644
index 0000000..ff8420a
--- /dev/null
+++ b/web/static/vendor/phoenix.js
@@ -0,0 +1,1068 @@
+(function(/*! Brunch !*/) {
+ 'use strict';
+
+ var globals = typeof window !== 'undefined' ? window : global;
+ if (typeof globals.require === 'function') return;
+
+ var modules = {};
+ var cache = {};
+
+ var has = function(object, name) {
+ return ({}).hasOwnProperty.call(object, name);
+ };
+
+ var expand = function(root, name) {
+ var results = [], parts, part;
+ if (/^\.\.?(\/|$)/.test(name)) {
+ parts = [root, name].join('/').split('/');
+ } else {
+ parts = name.split('/');
+ }
+ for (var i = 0, length = parts.length; i < length; i++) {
+ part = parts[i];
+ if (part === '..') {
+ results.pop();
+ } else if (part !== '.' && part !== '') {
+ results.push(part);
+ }
+ }
+ return results.join('/');
+ };
+
+ var dirname = function(path) {
+ return path.split('/').slice(0, -1).join('/');
+ };
+
+ var localRequire = function(path) {
+ return function(name) {
+ var dir = dirname(path);
+ var absolute = expand(dir, name);
+ return globals.require(absolute, path);
+ };
+ };
+
+ var initModule = function(name, definition) {
+ var module = {id: name, exports: {}};
+ cache[name] = module;
+ definition(module.exports, localRequire(name), module);
+ return module.exports;
+ };
+
+ var require = function(name, loaderPath) {
+ var path = expand(name, '.');
+ if (loaderPath == null) loaderPath = '/';
+
+ if (has(cache, path)) return cache[path].exports;
+ if (has(modules, path)) return initModule(path, modules[path]);
+
+ var dirIndex = expand(path, './index');
+ if (has(cache, dirIndex)) return cache[dirIndex].exports;
+ if (has(modules, dirIndex)) return initModule(dirIndex, modules[dirIndex]);
+
+ throw new Error('Cannot find module "' + name + '" from '+ '"' + loaderPath + '"');
+ };
+
+ var define = function(bundle, fn) {
+ if (typeof bundle === 'object') {
+ for (var key in bundle) {
+ if (has(bundle, key)) {
+ modules[key] = bundle[key];
+ }
+ }
+ } else {
+ modules[bundle] = fn;
+ }
+ };
+
+ var list = function() {
+ var result = [];
+ for (var item in modules) {
+ if (has(modules, item)) {
+ result.push(item);
+ }
+ }
+ return result;
+ };
+
+ globals.require = require;
+ globals.require.define = define;
+ globals.require.register = define;
+ globals.require.list = list;
+ globals.require.brunch = true;
+})();
+require.define({'phoenix': function(exports, require, module){ "use strict";
+
+var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); };
+
+var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
+
+// Phoenix Channels JavaScript client
+//
+// ## Socket Connection
+//
+// A single connection is established to the server and
+// channels are mulitplexed over the connection.
+// Connect to the server using the `Socket` class:
+//
+// let socket = new Socket("/ws")
+// socket.connect()
+//
+// The `Socket` constructor takes the mount point of the socket
+// as well as options that can be found in the Socket docs,
+// such as configuring the `LongPoller` transport, and heartbeat.
+//
+//
+// ## Channels
+//
+// Channels are isolated, concurrent processes on the server that
+// subscribe to topics and broker events between the client and server.
+// To join a channel, you must provide the topic, and channel params for
+// authorization. Here's an example chat room example where `"new_msg"`
+// events are listened for, messages are pushed to the server, and
+// the channel is joined with ok/error matches, and `after` hook:
+//
+// let chan = socket.chan("rooms:123", {token: roomToken})
+// chan.on("new_msg", msg => console.log("Got message", msg) )
+// $input.onEnter( e => {
+// chan.push("new_msg", {body: e.target.val})
+// .receive("ok", (message) => console.log("created message", message) )
+// .receive("error", (reasons) => console.log("create failed", reasons) )
+// .after(10000, () => console.log("Networking issue. Still waiting...") )
+// })
+// chan.join()
+// .receive("ok", ({messages}) => console.log("catching up", messages) )
+// .receive("error", ({reason}) => console.log("failed join", reason) )
+// .after(10000, () => console.log("Networking issue. Still waiting...") )
+//
+//
+// ## Joining
+//
+// Joining a channel with `chan.join(topic, params)`, binds the params to
+// `chan.params`. Subsequent rejoins will send up the modified params for
+// updating authorization params, or passing up last_message_id information.
+// Successful joins receive an "ok" status, while unsuccessful joins
+// receive "error".
+//
+//
+// ## Pushing Messages
+//
+// From the prevoius example, we can see that pushing messages to the server
+// can be done with `chan.push(eventName, payload)` and we can optionally
+// receive responses from the push. Additionally, we can use
+// `after(millsec, callback)` to abort waiting for our `receive` hooks and
+// take action after some period of waiting.
+//
+//
+// ## Socket Hooks
+//
+// Lifecycle events of the multiplexed connection can be hooked into via
+// `socket.onError()` and `socket.onClose()` events, ie:
+//
+// socket.onError( () => console.log("there was an error with the connection!") )
+// socket.onClose( () => console.log("the connection dropped") )
+//
+//
+// ## Channel Hooks
+//
+// For each joined channel, you can bind to `onError` and `onClose` events
+// to monitor the channel lifecycle, ie:
+//
+// chan.onError( () => console.log("there was an error!") )
+// chan.onClose( () => console.log("the channel has gone away gracefully") )
+//
+// ### onError hooks
+//
+// `onError` hooks are invoked if the socket connection drops, or the channel
+// crashes on the server. In either case, a channel rejoin is attemtped
+// automatically in an exponential backoff manner.
+//
+// ### onClose hooks
+//
+// `onClose` hooks are invoked only in two cases. 1) the channel explicitly
+// closed on the server, or 2). The client explicitly closed, by calling
+// `chan.leave()`
+//
+
+var SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 };
+var CHAN_STATES = {
+ closed: "closed",
+ errored: "errored",
+ joined: "joined",
+ joining: "joining" };
+var CHAN_EVENTS = {
+ close: "phx_close",
+ error: "phx_error",
+ join: "phx_join",
+ reply: "phx_reply",
+ leave: "phx_leave"
+};
+
+var Push = (function () {
+
+ // Initializes the Push
+ //
+ // chan - The Channel
+ // event - The event, ie `"phx_join"`
+ // payload - The payload, ie `{user_id: 123}`
+ //
+
+ function Push(chan, event, payload) {
+ _classCallCheck(this, Push);
+
+ this.chan = chan;
+ this.event = event;
+ this.payload = payload || {};
+ this.receivedResp = null;
+ this.afterHook = null;
+ this.recHooks = [];
+ this.sent = false;
+ }
+
+ _prototypeProperties(Push, null, {
+ send: {
+ value: function send() {
+ var _this = this;
+
+ var ref = this.chan.socket.makeRef();
+ this.refEvent = this.chan.replyEventName(ref);
+ this.receivedResp = null;
+ this.sent = false;
+
+ this.chan.on(this.refEvent, function (payload) {
+ _this.receivedResp = payload;
+ _this.matchReceive(payload);
+ _this.cancelRefEvent();
+ _this.cancelAfter();
+ });
+
+ this.startAfter();
+ this.sent = true;
+ this.chan.socket.push({
+ topic: this.chan.topic,
+ event: this.event,
+ payload: this.payload,
+ ref: ref
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ receive: {
+ value: function receive(status, callback) {
+ if (this.receivedResp && this.receivedResp.status === status) {
+ callback(this.receivedResp.response);
+ }
+
+ this.recHooks.push({ status: status, callback: callback });
+ return this;
+ },
+ writable: true,
+ configurable: true
+ },
+ after: {
+ value: function after(ms, callback) {
+ if (this.afterHook) {
+ throw "only a single after hook can be applied to a push";
+ }
+ var timer = null;
+ if (this.sent) {
+ timer = setTimeout(callback, ms);
+ }
+ this.afterHook = { ms: ms, callback: callback, timer: timer };
+ return this;
+ },
+ writable: true,
+ configurable: true
+ },
+ matchReceive: {
+
+ // private
+
+ value: function matchReceive(_ref) {
+ var status = _ref.status;
+ var response = _ref.response;
+ var ref = _ref.ref;
+
+ this.recHooks.filter(function (h) {
+ return h.status === status;
+ }).forEach(function (h) {
+ return h.callback(response);
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ cancelRefEvent: {
+ value: function cancelRefEvent() {
+ this.chan.off(this.refEvent);
+ },
+ writable: true,
+ configurable: true
+ },
+ cancelAfter: {
+ value: function cancelAfter() {
+ if (!this.afterHook) {
+ return;
+ }
+ clearTimeout(this.afterHook.timer);
+ this.afterHook.timer = null;
+ },
+ writable: true,
+ configurable: true
+ },
+ startAfter: {
+ value: function startAfter() {
+ var _this = this;
+
+ if (!this.afterHook) {
+ return;
+ }
+ var callback = function () {
+ _this.cancelRefEvent();
+ _this.afterHook.callback();
+ };
+ this.afterHook.timer = setTimeout(callback, this.afterHook.ms);
+ },
+ writable: true,
+ configurable: true
+ }
+ });
+
+ return Push;
+})();
+
+var Channel = exports.Channel = (function () {
+ function Channel(topic, params, socket) {
+ var _this = this;
+
+ _classCallCheck(this, Channel);
+
+ this.state = CHAN_STATES.closed;
+ this.topic = topic;
+ this.params = params || {};
+ this.socket = socket;
+ this.bindings = [];
+ this.joinedOnce = false;
+ this.joinPush = new Push(this, CHAN_EVENTS.join, this.params);
+ this.pushBuffer = [];
+
+ this.joinPush.receive("ok", function () {
+ _this.state = CHAN_STATES.joined;
+ });
+ this.onClose(function () {
+ _this.state = CHAN_STATES.closed;
+ _this.socket.remove(_this);
+ });
+ this.onError(function (reason) {
+ _this.state = CHAN_STATES.errored;
+ setTimeout(function () {
+ return _this.rejoinUntilConnected();
+ }, _this.socket.reconnectAfterMs);
+ });
+ this.on(CHAN_EVENTS.reply, function (payload, ref) {
+ _this.trigger(_this.replyEventName(ref), payload);
+ });
+ }
+
+ _prototypeProperties(Channel, null, {
+ rejoinUntilConnected: {
+ value: function rejoinUntilConnected() {
+ var _this = this;
+
+ if (this.state !== CHAN_STATES.errored) {
+ return;
+ }
+ if (this.socket.isConnected()) {
+ this.rejoin();
+ } else {
+ setTimeout(function () {
+ return _this.rejoinUntilConnected();
+ }, this.socket.reconnectAfterMs);
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ join: {
+ value: function join() {
+ if (this.joinedOnce) {
+ throw "tried to join mulitple times. 'join' can only be called a singe time per channel instance";
+ } else {
+ this.joinedOnce = true;
+ }
+ this.sendJoin();
+ return this.joinPush;
+ },
+ writable: true,
+ configurable: true
+ },
+ onClose: {
+ value: function onClose(callback) {
+ this.on(CHAN_EVENTS.close, callback);
+ },
+ writable: true,
+ configurable: true
+ },
+ onError: {
+ value: function onError(callback) {
+ this.on(CHAN_EVENTS.error, function (reason) {
+ return callback(reason);
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ on: {
+ value: function on(event, callback) {
+ this.bindings.push({ event: event, callback: callback });
+ },
+ writable: true,
+ configurable: true
+ },
+ off: {
+ value: function off(event) {
+ this.bindings = this.bindings.filter(function (bind) {
+ return bind.event !== event;
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ canPush: {
+ value: function canPush() {
+ return this.socket.isConnected() && this.state === CHAN_STATES.joined;
+ },
+ writable: true,
+ configurable: true
+ },
+ push: {
+ value: function push(event, payload) {
+ if (!this.joinedOnce) {
+ throw "tried to push '" + event + "' to '" + this.topic + "' before joining. Use chan.join() before pushing events";
+ }
+ var pushEvent = new Push(this, event, payload);
+ if (this.canPush()) {
+ pushEvent.send();
+ } else {
+ this.pushBuffer.push(pushEvent);
+ }
+
+ return pushEvent;
+ },
+ writable: true,
+ configurable: true
+ },
+ leave: {
+
+ // Leaves the channel
+ //
+ // Unsubscribes from server events, and
+ // instructs channel to terminate on server
+ //
+ // Triggers onClose() hooks
+ //
+ // To receive leave acknowledgements, use the a `receive`
+ // hook to bind to the server ack, ie:
+ //
+ // chan.leave().receive("ok", () => alert("left!") )
+ //
+
+ value: function leave() {
+ var _this = this;
+
+ return this.push(CHAN_EVENTS.leave).receive("ok", function () {
+ _this.trigger(CHAN_EVENTS.close, "leave");
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ isMember: {
+
+ // private
+
+ value: function isMember(topic) {
+ return this.topic === topic;
+ },
+ writable: true,
+ configurable: true
+ },
+ sendJoin: {
+ value: function sendJoin() {
+ this.state = CHAN_STATES.joining;
+ this.joinPush.send();
+ },
+ writable: true,
+ configurable: true
+ },
+ rejoin: {
+ value: function rejoin() {
+ this.sendJoin();
+ this.pushBuffer.forEach(function (pushEvent) {
+ return pushEvent.send();
+ });
+ this.pushBuffer = [];
+ },
+ writable: true,
+ configurable: true
+ },
+ trigger: {
+ value: function trigger(triggerEvent, payload, ref) {
+ this.bindings.filter(function (bind) {
+ return bind.event === triggerEvent;
+ }).map(function (bind) {
+ return bind.callback(payload, ref);
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ replyEventName: {
+ value: function replyEventName(ref) {
+ return "chan_reply_" + ref;
+ },
+ writable: true,
+ configurable: true
+ }
+ });
+
+ return Channel;
+})();
+
+var Socket = exports.Socket = (function () {
+
+ // Initializes the Socket
+ //
+ // endPoint - The string WebSocket endpoint, ie, "ws://example.com/ws",
+ // "wss://example.com"
+ // "/ws" (inherited host & protocol)
+ // opts - Optional configuration
+ // transport - The Websocket Transport, ie WebSocket, Phoenix.LongPoller.
+ // Defaults to WebSocket with automatic LongPoller fallback.
+ // heartbeatIntervalMs - The millisec interval to send a heartbeat message
+ // reconnectAfterMs - The millisec interval to reconnect after connection loss
+ // logger - The optional function for specialized logging, ie:
+ // `logger: function(msg){ console.log(msg) }`
+ // longpoller_timeout - The maximum timeout of a long poll AJAX request.
+ // Defaults to 20s (double the server long poll timer).
+ //
+ // For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
+ //
+
+ function Socket(endPoint) {
+ var opts = arguments[1] === undefined ? {} : arguments[1];
+
+ _classCallCheck(this, Socket);
+
+ this.stateChangeCallbacks = { open: [], close: [], error: [], message: [] };
+ this.reconnectTimer = null;
+ this.channels = [];
+ this.sendBuffer = [];
+ this.ref = 0;
+ this.transport = opts.transport || window.WebSocket || LongPoller;
+ this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000;
+ this.reconnectAfterMs = opts.reconnectAfterMs || 5000;
+ this.logger = opts.logger || function () {}; // noop
+ this.longpoller_timeout = opts.longpoller_timeout || 20000;
+ this.endPoint = this.expandEndpoint(endPoint);
+ }
+
+ _prototypeProperties(Socket, null, {
+ protocol: {
+ value: function protocol() {
+ return location.protocol.match(/^https/) ? "wss" : "ws";
+ },
+ writable: true,
+ configurable: true
+ },
+ expandEndpoint: {
+ value: function expandEndpoint(endPoint) {
+ if (endPoint.charAt(0) !== "/") {
+ return endPoint;
+ }
+ if (endPoint.charAt(1) === "/") {
+ return "" + this.protocol() + ":" + endPoint;
+ }
+
+ return "" + this.protocol() + "://" + location.host + "" + endPoint;
+ },
+ writable: true,
+ configurable: true
+ },
+ disconnect: {
+ value: function disconnect(callback, code, reason) {
+ if (this.conn) {
+ this.conn.onclose = function () {}; // noop
+ if (code) {
+ this.conn.close(code, reason || "");
+ } else {
+ this.conn.close();
+ }
+ this.conn = null;
+ }
+ callback && callback();
+ },
+ writable: true,
+ configurable: true
+ },
+ connect: {
+ value: function connect() {
+ var _this = this;
+
+ this.disconnect(function () {
+ _this.conn = new _this.transport(_this.endPoint);
+ _this.conn.timeout = _this.longpoller_timeout;
+ _this.conn.onopen = function () {
+ return _this.onConnOpen();
+ };
+ _this.conn.onerror = function (error) {
+ return _this.onConnError(error);
+ };
+ _this.conn.onmessage = function (event) {
+ return _this.onConnMessage(event);
+ };
+ _this.conn.onclose = function (event) {
+ return _this.onConnClose(event);
+ };
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ log: {
+
+ // Logs the message. Override `this.logger` for specialized logging. noops by default
+
+ value: function log(msg) {
+ this.logger(msg);
+ },
+ writable: true,
+ configurable: true
+ },
+ onOpen: {
+
+ // Registers callbacks for connection state change events
+ //
+ // Examples
+ //
+ // socket.onError(function(error){ alert("An error occurred") })
+ //
+
+ value: function onOpen(callback) {
+ this.stateChangeCallbacks.open.push(callback);
+ },
+ writable: true,
+ configurable: true
+ },
+ onClose: {
+ value: function onClose(callback) {
+ this.stateChangeCallbacks.close.push(callback);
+ },
+ writable: true,
+ configurable: true
+ },
+ onError: {
+ value: function onError(callback) {
+ this.stateChangeCallbacks.error.push(callback);
+ },
+ writable: true,
+ configurable: true
+ },
+ onMessage: {
+ value: function onMessage(callback) {
+ this.stateChangeCallbacks.message.push(callback);
+ },
+ writable: true,
+ configurable: true
+ },
+ onConnOpen: {
+ value: function onConnOpen() {
+ var _this = this;
+
+ this.flushSendBuffer();
+ clearInterval(this.reconnectTimer);
+ if (!this.conn.skipHeartbeat) {
+ clearInterval(this.heartbeatTimer);
+ this.heartbeatTimer = setInterval(function () {
+ return _this.sendHeartbeat();
+ }, this.heartbeatIntervalMs);
+ }
+ this.stateChangeCallbacks.open.forEach(function (callback) {
+ return callback();
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ onConnClose: {
+ value: function onConnClose(event) {
+ var _this = this;
+
+ this.log("WS close:");
+ this.log(event);
+ this.triggerChanError();
+ clearInterval(this.reconnectTimer);
+ clearInterval(this.heartbeatTimer);
+ this.reconnectTimer = setInterval(function () {
+ return _this.connect();
+ }, this.reconnectAfterMs);
+ this.stateChangeCallbacks.close.forEach(function (callback) {
+ return callback(event);
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ onConnError: {
+ value: function onConnError(error) {
+ this.log("WS error:");
+ this.log(error);
+ this.triggerChanError();
+ this.stateChangeCallbacks.error.forEach(function (callback) {
+ return callback(error);
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ triggerChanError: {
+ value: function triggerChanError() {
+ this.channels.forEach(function (chan) {
+ return chan.trigger(CHAN_EVENTS.error);
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ connectionState: {
+ value: function connectionState() {
+ switch (this.conn && this.conn.readyState) {
+ case SOCKET_STATES.connecting:
+ return "connecting";
+ case SOCKET_STATES.open:
+ return "open";
+ case SOCKET_STATES.closing:
+ return "closing";
+ default:
+ return "closed";
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ isConnected: {
+ value: function isConnected() {
+ return this.connectionState() === "open";
+ },
+ writable: true,
+ configurable: true
+ },
+ remove: {
+ value: function remove(chan) {
+ this.channels = this.channels.filter(function (c) {
+ return !c.isMember(chan.topic);
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ chan: {
+ value: function chan(topic, params) {
+ var chan = new Channel(topic, params, this);
+ this.channels.push(chan);
+ return chan;
+ },
+ writable: true,
+ configurable: true
+ },
+ push: {
+ value: function push(data) {
+ var _this = this;
+
+ var callback = function () {
+ return _this.conn.send(JSON.stringify(data));
+ };
+ if (this.isConnected()) {
+ callback();
+ } else {
+ this.sendBuffer.push(callback);
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ makeRef: {
+
+ // Return the next message ref, accounting for overflows
+
+ value: function makeRef() {
+ var newRef = this.ref + 1;
+ if (newRef === this.ref) {
+ this.ref = 0;
+ } else {
+ this.ref = newRef;
+ }
+
+ return this.ref.toString();
+ },
+ writable: true,
+ configurable: true
+ },
+ sendHeartbeat: {
+ value: function sendHeartbeat() {
+ this.push({ topic: "phoenix", event: "heartbeat", payload: {}, ref: this.makeRef() });
+ },
+ writable: true,
+ configurable: true
+ },
+ flushSendBuffer: {
+ value: function flushSendBuffer() {
+ if (this.isConnected() && this.sendBuffer.length > 0) {
+ this.sendBuffer.forEach(function (callback) {
+ return callback();
+ });
+ this.sendBuffer = [];
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ onConnMessage: {
+ value: function onConnMessage(rawMessage) {
+ this.log("message received:");
+ this.log(rawMessage);
+ var msg = JSON.parse(rawMessage.data);
+ var topic = msg.topic;
+ var event = msg.event;
+ var payload = msg.payload;
+ var ref = msg.ref;
+
+ this.channels.filter(function (chan) {
+ return chan.isMember(topic);
+ }).forEach(function (chan) {
+ return chan.trigger(event, payload, ref);
+ });
+ this.stateChangeCallbacks.message.forEach(function (callback) {
+ return callback(msg);
+ });
+ },
+ writable: true,
+ configurable: true
+ }
+ });
+
+ return Socket;
+})();
+
+var LongPoller = exports.LongPoller = (function () {
+ function LongPoller(endPoint) {
+ _classCallCheck(this, LongPoller);
+
+ this.retryInMs = 5000;
+ this.endPoint = null;
+ this.token = null;
+ this.sig = null;
+ this.skipHeartbeat = true;
+ this.onopen = function () {}; // noop
+ this.onerror = function () {}; // noop
+ this.onmessage = function () {}; // noop
+ this.onclose = function () {}; // noop
+ this.upgradeEndpoint = this.normalizeEndpoint(endPoint);
+ this.pollEndpoint = this.upgradeEndpoint + (/\/$/.test(endPoint) ? "poll" : "/poll");
+ this.readyState = SOCKET_STATES.connecting;
+
+ this.poll();
+ }
+
+ _prototypeProperties(LongPoller, null, {
+ normalizeEndpoint: {
+ value: function normalizeEndpoint(endPoint) {
+ return endPoint.replace("ws://", "http://").replace("wss://", "https://");
+ },
+ writable: true,
+ configurable: true
+ },
+ endpointURL: {
+ value: function endpointURL() {
+ return this.pollEndpoint + ("?token=" + encodeURIComponent(this.token) + "&sig=" + encodeURIComponent(this.sig));
+ },
+ writable: true,
+ configurable: true
+ },
+ closeAndRetry: {
+ value: function closeAndRetry() {
+ this.close();
+ this.readyState = SOCKET_STATES.connecting;
+ },
+ writable: true,
+ configurable: true
+ },
+ ontimeout: {
+ value: function ontimeout() {
+ this.onerror("timeout");
+ this.closeAndRetry();
+ },
+ writable: true,
+ configurable: true
+ },
+ poll: {
+ value: function poll() {
+ var _this = this;
+
+ if (!(this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting)) {
+ return;
+ }
+
+ Ajax.request("GET", this.endpointURL(), "application/json", null, this.timeout, this.ontimeout.bind(this), function (resp) {
+ if (resp) {
+ var status = resp.status;
+ var token = resp.token;
+ var sig = resp.sig;
+ var messages = resp.messages;
+
+ _this.token = token;
+ _this.sig = sig;
+ } else {
+ var status = 0;
+ }
+
+ switch (status) {
+ case 200:
+ messages.forEach(function (msg) {
+ return _this.onmessage({ data: JSON.stringify(msg) });
+ });
+ _this.poll();
+ break;
+ case 204:
+ _this.poll();
+ break;
+ case 410:
+ _this.readyState = SOCKET_STATES.open;
+ _this.onopen();
+ _this.poll();
+ break;
+ case 0:
+ case 500:
+ _this.onerror();
+ _this.closeAndRetry();
+ break;
+ default:
+ throw "unhandled poll status " + status;
+ }
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ send: {
+ value: function send(body) {
+ var _this = this;
+
+ Ajax.request("POST", this.endpointURL(), "application/json", body, this.timeout, this.onerror.bind(this, "timeout"), function (resp) {
+ if (!resp || resp.status !== 200) {
+ _this.onerror(status);
+ _this.closeAndRetry();
+ }
+ });
+ },
+ writable: true,
+ configurable: true
+ },
+ close: {
+ value: function close(code, reason) {
+ this.readyState = SOCKET_STATES.closed;
+ this.onclose();
+ },
+ writable: true,
+ configurable: true
+ }
+ });
+
+ return LongPoller;
+})();
+
+var Ajax = exports.Ajax = (function () {
+ function Ajax() {
+ _classCallCheck(this, Ajax);
+ }
+
+ _prototypeProperties(Ajax, {
+ request: {
+ value: function request(method, endPoint, accept, body, timeout, ontimeout, callback) {
+ if (window.XDomainRequest) {
+ var req = new XDomainRequest(); // IE8, IE9
+ this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback);
+ } else {
+ var req = window.XMLHttpRequest ? new XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari
+ new ActiveXObject("Microsoft.XMLHTTP"); // IE6, IE5
+ this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback);
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ xdomainRequest: {
+ value: function xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) {
+ var _this = this;
+
+ req.timeout = timeout;
+ req.open(method, endPoint);
+ req.onload = function () {
+ var response = _this.parseJSON(req.responseText);
+ callback && callback(response);
+ };
+ if (ontimeout) {
+ req.ontimeout = ontimeout;
+ }
+
+ // Work around bug in IE9 that requires an attached onprogress handler
+ req.onprogress = function () {};
+
+ req.send(body);
+ },
+ writable: true,
+ configurable: true
+ },
+ xhrRequest: {
+ value: function xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback) {
+ var _this = this;
+
+ req.timeout = timeout;
+ req.open(method, endPoint, true);
+ req.setRequestHeader("Content-Type", accept);
+ req.onerror = function () {
+ callback && callback(null);
+ };
+ req.onreadystatechange = function () {
+ if (req.readyState === _this.states.complete && callback) {
+ var response = _this.parseJSON(req.responseText);
+ callback(response);
+ }
+ };
+ if (ontimeout) {
+ req.ontimeout = ontimeout;
+ }
+
+ req.send(body);
+ },
+ writable: true,
+ configurable: true
+ },
+ parseJSON: {
+ value: function parseJSON(resp) {
+ return resp && resp !== "" ? JSON.parse(resp) : null;
+ },
+ writable: true,
+ configurable: true
+ }
+ });
+
+ return Ajax;
+})();
+
+Ajax.states = { complete: 4 };
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+ }});
+if(typeof(window) === 'object' && !window.Phoenix){ window.Phoenix = require('phoenix') };
\ No newline at end of file
diff --git a/web/templates/layout/application.html.eex b/web/templates/layout/application.html.eex
new file mode 100644
index 0000000..8ab7880
--- /dev/null
+++ b/web/templates/layout/application.html.eex
@@ -0,0 +1,27 @@
+
+
+
+ This page exists because I think we need a site where people can post what they made in Elixir, simliar to Rubyflow for Ruby.
+ A site that is build in Elixir, that is Open Source and integrates nicely with the existing ecosystem.
+
+
+ I think a site like that, if done right, will eventually be of great use to encourage people to contribute to the Elixir community.
+ You can read the full reasoning behind this in the project proposal.
+
+
+ Ah, nearly forgot: I want to build this site, right here.
+
diff --git a/web/templates/page/index_not_logged_in.html.eex b/web/templates/page/index_not_logged_in.html.eex
new file mode 100644
index 0000000..2232a4a
--- /dev/null
+++ b/web/templates/page/index_not_logged_in.html.eex
@@ -0,0 +1,8 @@
+
+
+ By signing in, you show that you would like this site to exist.
+
+
+
+ Sign in with GitHub
+
diff --git a/web/views/error_view.ex b/web/views/error_view.ex
new file mode 100644
index 0000000..72d9d46
--- /dev/null
+++ b/web/views/error_view.ex
@@ -0,0 +1,17 @@
+defmodule ElixirStatus.ErrorView do
+ use ElixirStatus.Web, :view
+
+ def render("404.html", _assigns) do
+ "Page not found"
+ end
+
+ def render("500.html", _assigns) do
+ "Server internal error"
+ end
+
+ # In case no render clause matches or no
+ # template is found, let's render it as 500
+ def template_not_found(_template, assigns) do
+ render "500.html", assigns
+ end
+end
diff --git a/web/views/layout_view.ex b/web/views/layout_view.ex
new file mode 100644
index 0000000..595c5f3
--- /dev/null
+++ b/web/views/layout_view.ex
@@ -0,0 +1,3 @@
+defmodule ElixirStatus.LayoutView do
+ use ElixirStatus.Web, :view
+end
diff --git a/web/views/page_view.ex b/web/views/page_view.ex
new file mode 100644
index 0000000..608fdac
--- /dev/null
+++ b/web/views/page_view.ex
@@ -0,0 +1,3 @@
+defmodule ElixirStatus.PageView do
+ use ElixirStatus.Web, :view
+end
diff --git a/web/web.ex b/web/web.ex
new file mode 100644
index 0000000..1d6a87a
--- /dev/null
+++ b/web/web.ex
@@ -0,0 +1,78 @@
+defmodule ElixirStatus.Web do
+ @moduledoc """
+ A module that keeps using definitions for controllers,
+ views and so on.
+
+ This can be used in your application as:
+
+ use ElixirStatus.Web, :controller
+ use ElixirStatus.Web, :view
+
+ The definitions below will be executed for every view,
+ controller, etc, so keep them short and clean, focused
+ on imports, uses and aliases.
+
+ Do NOT define functions inside the quoted expressions
+ below.
+ """
+
+ def model do
+ quote do
+ use Ecto.Model
+ end
+ end
+
+ def controller do
+ quote do
+ use Phoenix.Controller
+
+ # Alias the data repository and import query/model functions
+ alias ElixirStatus.Repo
+ import Ecto.Model
+ import Ecto.Query, only: [from: 2]
+
+ # Import URL helpers from the router
+ import ElixirStatus.Router.Helpers
+ end
+ end
+
+ def view do
+ quote do
+ use Phoenix.View, root: "web/templates"
+
+ # Import convenience functions from controllers
+ import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
+
+ # Import URL helpers from the router
+ import ElixirStatus.Router.Helpers
+
+ # Use all HTML functionality (forms, tags, etc)
+ use Phoenix.HTML
+ end
+ end
+
+ def router do
+ quote do
+ use Phoenix.Router
+ end
+ end
+
+ def channel do
+ quote do
+ use Phoenix.Channel
+
+ # Alias the data repository and import query/model functions
+ alias ElixirStatus.Repo
+ import Ecto.Model
+ import Ecto.Query, only: [from: 2]
+
+ end
+ end
+
+ @doc """
+ When used, dispatch to the appropriate controller/view/etc.
+ """
+ defmacro __using__(which) when is_atom(which) do
+ apply(__MODULE__, which, [])
+ end
+end