├── assets
├── static
│ ├── favicon.ico
│ ├── images
│ │ └── phoenix.png
│ └── robots.txt
├── package.json
├── js
│ ├── app.js
│ ├── components
│ │ └── todo
│ │ │ ├── todo-new.js
│ │ │ ├── todo-item.js
│ │ │ └── index.js
│ └── socket.js
├── css
│ ├── app.scss
│ └── phoenix.css
└── brunch-config.js
├── lib
├── ztd_web
│ ├── views
│ │ ├── todo_view.ex
│ │ ├── layout_view.ex
│ │ ├── error_view.ex
│ │ └── error_helpers.ex
│ ├── templates
│ │ ├── todo
│ │ │ └── index.html.eex
│ │ └── layout
│ │ │ └── app.html.eex
│ ├── controllers
│ │ └── todo_controller.ex
│ ├── router.ex
│ ├── gettext.ex
│ ├── channels
│ │ ├── sockets_main.ex
│ │ └── todo_events.ex
│ └── endpoint.ex
├── ztd.ex
├── ztd
│ ├── repo
│ │ ├── repo.ex
│ │ └── schema.ex
│ ├── todo
│ │ ├── engine
│ │ │ ├── schema.ex
│ │ │ ├── engine.ex
│ │ │ ├── broadcaster.ex
│ │ │ └── listener.ex
│ │ ├── worker
│ │ │ ├── worker.ex
│ │ │ ├── dispatcher.ex
│ │ │ ├── rpc.ex
│ │ │ └── listener.ex
│ │ ├── todo.ex
│ │ ├── event.ex
│ │ ├── config.ex
│ │ └── supervisor.ex
│ └── application.ex
└── ztd_web.ex
├── test
├── test_helper.exs
├── ztd_web
│ ├── views
│ │ └── error_test.exs
│ ├── controllers
│ │ └── todo_test.exs
│ └── channels
│ │ └── todo_events_test.exs
├── support
│ ├── cases
│ │ ├── case.ex
│ │ ├── channel_case.ex
│ │ └── conn_case.ex
│ └── support.ex
└── ztd
│ └── todo
│ ├── event_test.exs
│ ├── engine_test.exs
│ └── config_test.exs
├── .travis.yml
├── Procfile
├── priv
├── repo
│ ├── seeds.exs
│ └── migrations
│ │ └── 20180610101141_create_todo_items.exs
└── gettext
│ ├── en
│ └── LC_MESSAGES
│ │ └── errors.po
│ └── errors.pot
├── config
├── test.exs
├── config.exs
├── dev.exs
└── prod.exs
├── .gitignore
├── .iex.exs
├── LICENSE
├── mix.exs
├── README.md
└── mix.lock
/assets/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sheharyarn/ztd/HEAD/assets/static/favicon.ico
--------------------------------------------------------------------------------
/lib/ztd_web/views/todo_view.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Web.Views.Todo do
2 | use ZTD.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
3 | Ecto.Adapters.SQL.Sandbox.mode(ZTD.Repo, :manual)
4 |
5 |
--------------------------------------------------------------------------------
/assets/static/images/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sheharyarn/ztd/HEAD/assets/static/images/phoenix.png
--------------------------------------------------------------------------------
/lib/ztd_web/templates/todo/index.html.eex:
--------------------------------------------------------------------------------
1 |
2 | <%= React.react_component("Components.Todo", %{items: @items, mode: @mode}) %>
3 |
4 |
--------------------------------------------------------------------------------
/lib/ztd_web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Web.Views.Layout do
2 | use ZTD.Web, :view
3 |
4 |
5 | def title do
6 | "ZTD Todo App"
7 | end
8 |
9 | end
10 |
--------------------------------------------------------------------------------
/assets/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: elixir
2 | elixir:
3 | - 1.6.0
4 | otp_release:
5 | - 20.0
6 | services:
7 | - rabbitmq
8 | sudo: false
9 | env:
10 | global:
11 | - HEX_HTTP_CONCURRENCY=1
12 | - HEX_HTTP_TIMEOUT=120
13 | script:
14 | - mix test
15 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | engine: env PORT=4000 APP_MODE=engine mix phx.server
2 | worker_1: env PORT=5001 APP_MODE=worker mix phx.server
3 | worker_2: env PORT=5002 APP_MODE=worker mix phx.server
4 | worker_3: env PORT=5003 APP_MODE=worker mix phx.server
5 | worker_4: env PORT=5004 APP_MODE=worker mix phx.server
6 |
--------------------------------------------------------------------------------
/lib/ztd.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD do
2 | @moduledoc """
3 | ZTD keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 | end
10 |
--------------------------------------------------------------------------------
/lib/ztd/repo/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Repo do
2 | use Ecto.Repo, otp_app: :ztd
3 |
4 | @doc """
5 | Dynamically loads the repository url from the
6 | DATABASE_URL environment variable.
7 | """
8 | def init(_, opts) do
9 | {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/ztd_web/controllers/todo_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Web.Controllers.Todo do
2 | use ZTD.Web, :controller
3 |
4 | alias ZTD.Todo
5 |
6 |
7 | @doc "GET: All Todo Items"
8 | def index(conn, _params) do
9 | render(conn, "index.html", items: Todo.all, mode: Todo.Config.mode)
10 | end
11 |
12 | end
13 |
--------------------------------------------------------------------------------
/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 | # ZTD.Repo.insert!(%ZTD.SomeSchema{})
9 | #
10 | # We recommend using the bang functions (`insert!`, `update!`
11 | # and so on) as they will fail if something goes wrong.
12 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20180610101141_create_todo_items.exs:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Repo.Migrations.CreateTodoItems do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:todo_items, primary_key: false) do
6 | add :id, :binary_id, primary_key: true
7 | add :title, :string, null: false
8 | add :done, :boolean, null: false, default: false
9 |
10 | timestamps()
11 | end
12 | end
13 |
14 | end
15 |
--------------------------------------------------------------------------------
/test/ztd_web/views/error_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Web.ErrorViewTest do
2 | use ZTD.Tests.Support.ConnCase, async: true
3 | import Phoenix.View
4 |
5 |
6 | describe "Error Pages" do
7 | @view ZTD.Web.Views.Error
8 |
9 | test "renders 404.html" do
10 | assert render_to_string(@view, "404.html", []) == "Not Found"
11 | end
12 |
13 | test "renders 500.html" do
14 | assert render_to_string(@view, "500.html", []) == "Internal Server Error"
15 | end
16 | end
17 |
18 | end
19 |
--------------------------------------------------------------------------------
/test/support/cases/case.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Tests.Support.Case do
2 | use ExUnit.CaseTemplate
3 |
4 | @moduledoc """
5 | Default Test Case with important aliases/imports
6 | """
7 |
8 |
9 | using do
10 | quote do
11 | alias ZTD.Repo
12 | alias ZTD.Tests.Support
13 |
14 | alias Ecto.Query
15 | alias Ecto.Changeset
16 |
17 | require Query
18 | import Support.Schema
19 | end
20 | end
21 |
22 |
23 | setup tags do
24 | ZTD.Tests.Support.setup_ecto(tags)
25 | end
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/lib/ztd/repo/schema.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Repo.Schema do
2 |
3 | @moduledoc """
4 | Custom Macro for initializing Schemas with
5 | sane defaults
6 | """
7 |
8 | defmacro __using__(_opts) do
9 | quote do
10 | use Ecto.Schema
11 | use Ecto.Rut, repo: ZTD.Repo
12 |
13 | @primary_key {:id, :binary_id, autogenerate: true}
14 | @foreign_key_type :binary_id
15 |
16 | import Ecto.Changeset
17 | require Ecto.Query
18 |
19 | alias Ecto.Query
20 | alias ZTD.Repo
21 | end
22 | end
23 |
24 | end
25 |
26 |
--------------------------------------------------------------------------------
/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 :ztd, ZTD.Web.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 :ztd, ZTD.Repo,
14 | adapter: Ecto.Adapters.Postgres,
15 | username: "postgres",
16 | password: "postgres",
17 | database: "ztd_test",
18 | hostname: "localhost",
19 | pool: Ecto.Adapters.SQL.Sandbox
20 |
--------------------------------------------------------------------------------
/lib/ztd_web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Web.Views.Error do
2 | use ZTD.Web, :view
3 |
4 | # If you want to customize a particular status code
5 | # for a certain format, you may uncomment below.
6 | # def render("500.html", _assigns) do
7 | # "Internal Server Error"
8 | # end
9 |
10 | # By default, Phoenix returns the status message from
11 | # the template name. For example, "404.html" becomes
12 | # "Not Found".
13 | def template_not_found(template, _assigns) do
14 | Phoenix.Controller.status_message_from_template(template)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/ztd_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Web.Router do
2 | use ZTD.Web, :router
3 |
4 |
5 | # Pipelines
6 | # ---------
7 |
8 | pipeline :browser do
9 | plug :accepts, ["html"]
10 | plug :fetch_session
11 | plug :fetch_flash
12 | plug :protect_from_forgery
13 | plug :put_secure_browser_headers
14 | end
15 |
16 | pipeline :api do
17 | plug :accepts, ["json"]
18 | end
19 |
20 |
21 |
22 | # Routes
23 | # ------
24 |
25 | scope "/", ZTD.Web.Controllers do
26 | pipe_through :browser
27 |
28 | get "/", Todo, :index
29 | end
30 |
31 |
32 | end
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # App artifacts
2 | /_build
3 | /db
4 | /deps
5 | /*.ez
6 |
7 | # Generated on crash by the VM
8 | erl_crash.dump
9 |
10 | # Generated on crash by NPM
11 | npm-debug.log
12 |
13 | # Static artifacts
14 | /assets/node_modules
15 |
16 | # Since we are building assets from assets/,
17 | # we ignore priv/static. You may want to comment
18 | # this depending on your deployment strategy.
19 | /priv/static/
20 |
21 | # Files matching config/*.secret.exs pattern contain sensitive
22 | # data and you should not commit them into version control.
23 | #
24 | # Alternatively, you may comment the line below and commit the
25 | # secrets files as long as you replace their contents by environment
26 | # variables.
27 | /config/*.secret.exs
--------------------------------------------------------------------------------
/lib/ztd/todo/engine/schema.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Todo.Engine.Schema do
2 | use ZTD.Repo.Schema
3 |
4 |
5 |
6 | ## Schema
7 | ## ------
8 |
9 | @fields_required [:title]
10 | @fields_optional [:done]
11 | @fields_all (@fields_optional ++ @fields_required)
12 | @derive {Poison.Encoder, only: [:id | @fields_all]}
13 |
14 |
15 | schema "todo_items" do
16 | field :title, :string
17 | field :done, :boolean, default: false
18 |
19 | timestamps()
20 | end
21 |
22 |
23 |
24 |
25 | ## Public API
26 | ## ----------
27 |
28 |
29 | @doc "Create Changeset"
30 | def changeset(struct, attrs) do
31 | struct
32 | |> cast(attrs, @fields_all)
33 | |> validate_required(@fields_required)
34 | end
35 |
36 | end
37 |
--------------------------------------------------------------------------------
/assets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "repository": {},
3 | "license": "MIT",
4 | "scripts": {
5 | "deploy": "brunch build --production",
6 | "watch": "brunch watch --stdin"
7 | },
8 | "dependencies": {
9 | "babel-preset-env": "^1.7.0",
10 | "babel-preset-react": "^6.24.1",
11 | "lodash": "^4.17.10",
12 | "phoenix": "file:../deps/phoenix",
13 | "phoenix_html": "file:../deps/phoenix_html",
14 | "react": "^16.4.0",
15 | "react-dom": "^16.2.0",
16 | "react-phoenix": "file:../deps/react_phoenix",
17 | "sass-brunch": "^2.10.4"
18 | },
19 | "devDependencies": {
20 | "babel-brunch": "6.1.1",
21 | "brunch": "2.10.9",
22 | "clean-css-brunch": "2.10.0",
23 | "uglify-js-brunch": "2.10.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/ztd/application.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Application do
2 | use Application
3 |
4 |
5 | @doc "Start application supervision tree"
6 | def start(_type, _args) do
7 | import Supervisor.Spec
8 |
9 | children = [
10 | # Todo Sub App
11 | supervisor(ZTD.Todo.Supervisor, []),
12 |
13 | # Web Server
14 | supervisor(ZTD.Web.Endpoint, []),
15 | ]
16 |
17 | opts = [strategy: :one_for_one, name: ZTD.Supervisor]
18 | Supervisor.start_link(children, opts)
19 | end
20 |
21 |
22 | # Tell Phoenix to update the endpoint configuration
23 | # whenever the application is updated.
24 | def config_change(changed, _new, removed) do
25 | ZTD.Web.Endpoint.config_change(changed, removed)
26 | :ok
27 | end
28 |
29 | end
30 |
--------------------------------------------------------------------------------
/lib/ztd_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Web.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import ZTD.Web.Gettext
9 |
10 | # Simple translation
11 | gettext "Here is the string to translate"
12 |
13 | # Plural translation
14 | ngettext "Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3
17 |
18 | # Domain-based translation
19 | dgettext "errors", "Here is the error message to translate"
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :ztd
24 | end
25 |
--------------------------------------------------------------------------------
/.iex.exs:
--------------------------------------------------------------------------------
1 | alias ZTD.{Todo, Todo.Config, Todo.Engine, Todo.Worker}
2 | alias ZTD.{Web, Web.Channels.TodoEvents}
3 | alias ZTD.Repo
4 | alias Ecto.Query
5 |
6 | require Query
7 |
8 |
9 | request_queue = Config.get(:amqp)[:request_queue]
10 | request_exchange = Config.get(:amqp)[:request_exchange]
11 | broadcast_exchange = Config.get(:amqp)[:broadcast_exchange]
12 |
13 |
14 | engine_channel = fn ->
15 | {:ok, connection} = AMQP.Connection.open
16 | {:ok, channel} = AMQP.Channel.open(connection)
17 |
18 | AMQP.Exchange.declare(channel, request_exchange, :direct)
19 | AMQP.Queue.declare(channel, request_queue, durable: false)
20 | AMQP.Queue.bind(channel, request_queue, request_exchange)
21 |
22 | channel
23 | end
24 |
25 | engine_publish = fn channel, message ->
26 | AMQP.Basic.publish(channel, request_exchange, request_queue, message)
27 | end
28 |
29 |
--------------------------------------------------------------------------------
/assets/js/app.js:
--------------------------------------------------------------------------------
1 | // Brunch automatically concatenates all files in your
2 | // watched paths. Those paths can be configured at
3 | // config.paths.watched in "brunch-config.js".
4 | //
5 | // However, those files will only be executed if
6 | // explicitly imported. The only exception are files
7 | // in vendor, which are never wrapped in imports and
8 | // therefore are always executed.
9 |
10 | // Import dependencies
11 | //
12 | // If you no longer want to use a dependency, remember
13 | // to also remove its path from "config.paths.watched".
14 | import "phoenix_html"
15 | import "react-phoenix"
16 |
17 | // Import local files
18 | //
19 | // Local files can be imported directly using relative
20 | // paths "./socket" or full ones "web/static/js/socket".
21 |
22 | // import socket from "./socket"
23 |
24 |
25 | // React Components
26 | import Todo from "./components/todo"
27 |
28 | window.Components = {
29 | Todo,
30 | };
31 |
--------------------------------------------------------------------------------
/lib/ztd/todo/worker/worker.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Todo.Worker do
2 | alias ZTD.Todo.Event
3 | alias ZTD.Todo.Worker.RPC
4 | alias ZTD.Todo.Worker.Dispatcher
5 |
6 |
7 | @moduledoc """
8 | Worker implementation for the Todo interface.
9 | Implements all methods defined in the Todo module.
10 |
11 | Each action is wrapped in an event and dispatched
12 | to the engine.
13 | """
14 |
15 |
16 |
17 | @doc "Get all todos"
18 | def all do
19 | RPC.all
20 | end
21 |
22 |
23 | @doc "Insert new todo"
24 | def insert(%{} = params) do
25 | :insert
26 | |> Event.new(params)
27 | |> Dispatcher.send!
28 | end
29 |
30 |
31 | @doc "Update a todo"
32 | def update(id, %{} = params) do
33 | data = Map.put(params, :id, id)
34 |
35 | :update
36 | |> Event.new(data)
37 | |> Dispatcher.send!
38 | end
39 |
40 |
41 | @doc "Delete a todo"
42 | def delete(id) do
43 | :delete
44 | |> Event.new(%{id: id})
45 | |> Dispatcher.send!
46 | end
47 |
48 | end
49 |
--------------------------------------------------------------------------------
/lib/ztd/todo/todo.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Todo do
2 | import ZTD.Todo.Config, only: [adapter: 0]
3 |
4 |
5 | @moduledoc """
6 | Provides an interface to add, update, delete and
7 | mark items done/undone. Selects the correct adapter
8 | and delegates the method call to it.
9 |
10 | NOTE:
11 | If the application grows complex in the future, it
12 | would make sense to turn this into a behaviour and
13 | define a __using__ macro which implements it and
14 | verifies if all callbacks have been implemented.
15 | """
16 |
17 |
18 |
19 |
20 | @doc "Get all todos"
21 | def all do
22 | adapter().all()
23 | end
24 |
25 |
26 |
27 | @doc "Insert new todo"
28 | def insert(%{} = params) do
29 | adapter().insert(params)
30 | end
31 |
32 |
33 |
34 | @doc "Update a todo"
35 | def update(id, %{} = params) do
36 | adapter().update(id, params)
37 | end
38 |
39 |
40 |
41 | @doc "Delete a todo"
42 | def delete(id) do
43 | adapter().delete(id)
44 | end
45 |
46 |
47 | end
48 |
--------------------------------------------------------------------------------
/test/support/cases/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Tests.Support.ChannelCase do
2 | use ExUnit.CaseTemplate
3 |
4 | @moduledoc """
5 | This module defines the test case to be used by
6 | channel tests.
7 |
8 | Such tests rely on `Phoenix.ChannelTest` and also
9 | import other functionality to make it easier
10 | to build common datastructures and query the data layer.
11 |
12 | Finally, if the test case interacts with the database,
13 | it cannot be async. For this reason, every test runs
14 | inside a transaction which is reset at the beginning
15 | of the test unless the test case is marked as async.
16 | """
17 |
18 |
19 | using do
20 | quote do
21 | # Import conveniences for testing with channels
22 | use Phoenix.ChannelTest
23 |
24 | alias ZTD.Tests.Support
25 |
26 | # The default endpoint for testing
27 | @endpoint ZTD.Web.Endpoint
28 | end
29 | end
30 |
31 |
32 | setup tags do
33 | ZTD.Tests.Support.setup_ecto(tags)
34 | end
35 |
36 | end
37 |
--------------------------------------------------------------------------------
/lib/ztd_web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | <%= title() %>
11 | ">
12 |
13 |
14 |
15 |
16 |
19 |
20 |
<%= get_flash(@conn, :info) %>
21 |
<%= get_flash(@conn, :error) %>
22 |
23 |
24 | <%= render @view_module, @view_template, assigns %>
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/test/ztd_web/controllers/todo_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Tests.Web.Controllers.Todo do
2 | use ZTD.Tests.Support.ConnCase
3 |
4 | alias ZTD.Todo
5 |
6 |
7 | describe "#index" do
8 | @path Router.todo_path(@endpoint, :index)
9 |
10 | setup context do
11 | {:ok, _} = Todo.insert(%{title: "Pending Item", done: false})
12 | {:ok, _} = Todo.insert(%{title: "Done Item", done: true})
13 | context
14 | end
15 |
16 | test "renders the todo react component with todo items", %{conn: conn} do
17 | response =
18 | conn
19 | |> get(@path)
20 | |> html_response(200)
21 |
22 | assert response =~ ~r/data-react-class=.Components.Todo/
23 | assert response =~ ~r/Pending Item/
24 | assert response =~ ~r/Done Item/
25 | end
26 |
27 | test "renders the app mode as the component prop", %{conn: conn} do
28 | response =
29 | conn
30 | |> get(@path)
31 | |> html_response(200)
32 |
33 | assert response =~ ~r/#{Todo.Config.mode}/
34 | end
35 | end
36 |
37 | end
38 |
--------------------------------------------------------------------------------
/test/support/cases/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Tests.Support.ConnCase do
2 | use ExUnit.CaseTemplate
3 |
4 | @moduledoc """
5 | This module defines the test case to be used by
6 | tests that require setting up a connection.
7 |
8 | Such tests rely on `Phoenix.ConnTest` and also
9 | import other functionality to make it easier
10 | to build common datastructures and query the data layer.
11 |
12 | Finally, if the test case interacts with the database,
13 | it cannot be async. For this reason, every test runs
14 | inside a transaction which is reset at the beginning
15 | of the test unless the test case is marked as async.
16 | """
17 |
18 |
19 | using do
20 | quote do
21 | # Import conveniences for testing with connections
22 | use Phoenix.ConnTest
23 | alias ZTD.Web.Router.Helpers, as: Router
24 |
25 | # The default endpoint for testing
26 | @endpoint ZTD.Web.Endpoint
27 | end
28 | end
29 |
30 |
31 | setup tags do
32 | ZTD.Tests.Support.setup_ecto(tags)
33 | {:ok, conn: Phoenix.ConnTest.build_conn()}
34 | end
35 |
36 | end
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Sheharyar Naseer (https://sheharyar.me/)
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 |
23 |
--------------------------------------------------------------------------------
/lib/ztd/todo/event.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Todo.Event do
2 | defmodule InvalidError, do: defexception [:message]
3 |
4 | alias ZTD.Todo.Event
5 |
6 |
7 | @moduledoc """
8 | Event representing some sort of change to the Todo List
9 | """
10 |
11 |
12 | defstruct [:type, :data]
13 | @allowed_types [:insert, :update, :delete]
14 |
15 |
16 |
17 | @doc "Create new change event"
18 | def new(type, data) when type in @allowed_types do
19 | %Event{type: type, data: data}
20 | end
21 |
22 | def new(type, _data) do
23 | raise Event.InvalidError, message: "Invalid type: #{inspect type}"
24 | end
25 |
26 |
27 |
28 | @doc "Encode event to string"
29 | def encode!(%Event{} = event) do
30 | Poison.encode!(event)
31 | end
32 |
33 |
34 | @doc "Decode string back to event"
35 | def decode!(string) when is_binary(string) do
36 | contents =
37 | string
38 | |> Poison.decode!
39 | |> BetterParams.symbolize_merge(drop_string_keys: true)
40 |
41 | contents.type
42 | |> String.to_existing_atom
43 | |> new(contents.data)
44 | rescue
45 | _ ->
46 | raise Event.InvalidError, message: "Can't parse event: #{inspect string}"
47 | end
48 |
49 | end
50 |
--------------------------------------------------------------------------------
/assets/js/components/todo/todo-new.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 |
5 | class TodoNew extends React.Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {
10 | text: '',
11 | };
12 |
13 | this.handleChange = this.handleChange.bind(this);
14 | this.handleKeyPress = this.handleKeyPress.bind(this);
15 | }
16 |
17 |
18 | handleChange(e) {
19 | this.setState({text: e.target.value});
20 | }
21 |
22 |
23 | handleKeyPress(e) {
24 | if (e.key === 'Enter') {
25 | const {text} = this.state;
26 | this.props.broadcast("insert", {title: text});
27 | this.setState({text: ''});
28 | }
29 | }
30 |
31 |
32 | render() {
33 | const {text} = this.state;
34 |
35 | return (
36 |
37 |
44 |
45 | );
46 | }
47 | }
48 |
49 |
50 |
51 | // Prop Specification
52 | TodoNew.propTypes = {
53 | broadcast: PropTypes.func.isRequired,
54 | };
55 |
56 |
57 |
58 | // Export
59 | export default TodoNew;
60 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 |
4 | # General Application Config
5 | config :ztd,
6 | namespace: ZTD,
7 | ecto_repos: [ZTD.Repo],
8 | generators: [binary_id: true]
9 |
10 |
11 | # Todo Settings / Mode Adapters
12 | config :ztd, :todo,
13 | env_var: "APP_MODE",
14 | default: :engine,
15 | adapters: [
16 | engine: ZTD.Todo.Engine,
17 | worker: ZTD.Todo.Worker,
18 | ],
19 | amqp: [
20 | request_queue: "ztd.todo.requests.queue",
21 | request_exchange: "ztd.todo.requests.exchange",
22 | broadcast_exchange: "ztd.todo.broadcasts.exchange",
23 | broadcast_routing: "ztd.todo.broadcasts.routing",
24 | ]
25 |
26 |
27 | # Endpoint
28 | config :ztd, ZTD.Web.Endpoint,
29 | url: [host: "localhost"],
30 | secret_key_base: "Q6jgvP5PBpizrwNmRpmL+jufEpjbTV9JjEH+FC6HngbYlk2EThFD2CHKSAZ85jOy",
31 | render_errors: [view: ZTD.Web.ErrorView, accepts: ~w(html json)],
32 | pubsub: [name: ZTD.PubSub, adapter: Phoenix.PubSub.PG2]
33 |
34 |
35 | # Logger
36 | config :logger, :console,
37 | format: "$time $metadata[$level] $message\n",
38 | metadata: [:user_id]
39 |
40 |
41 | # Import environment specific config. This must remain at the bottom
42 | # of this file so it overrides the configuration defined above.
43 | import_config "#{Mix.env}.exs"
44 |
--------------------------------------------------------------------------------
/lib/ztd_web/channels/sockets_main.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Web.Sockets.Main do
2 | use Phoenix.Socket
3 |
4 | ## Channels
5 | channel "todo_events", ZTD.Web.Channels.TodoEvents
6 |
7 | ## Transports
8 | transport :websocket, Phoenix.Transports.WebSocket
9 | # transport :longpoll, Phoenix.Transports.LongPoll
10 |
11 | # Socket params are passed from the client and can
12 | # be used to verify and authenticate a user. After
13 | # verification, you can put default assigns into
14 | # the socket that will be set for all channels, ie
15 | #
16 | # {:ok, assign(socket, :user_id, verified_user_id)}
17 | #
18 | # To deny connection, return `:error`.
19 | #
20 | # See `Phoenix.Token` documentation for examples in
21 | # performing token verification on connect.
22 | def connect(_params, socket) do
23 | {:ok, socket}
24 | end
25 |
26 | # Socket id's are topics that allow you to identify all sockets for a given user:
27 | #
28 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
29 | #
30 | # Would allow you to broadcast a "disconnect" event and terminate
31 | # all active sockets and channels for a given user:
32 | #
33 | # ZTD.Web.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
34 | #
35 | # Returning `nil` makes this socket anonymous.
36 | def id(_socket), do: nil
37 | end
38 |
--------------------------------------------------------------------------------
/lib/ztd/todo/config.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Todo.Config do
2 | @moduledoc """
3 | Interface for getting Todo Configs
4 | """
5 |
6 | @app_name :ztd
7 | @config_key :todo
8 |
9 |
10 |
11 |
12 |
13 | ## Public API
14 | ## ----------
15 |
16 |
17 | @doc "Get Todo Config"
18 | def get do
19 | Application.get_env(@app_name, @config_key)
20 | end
21 |
22 |
23 | @doc "Get Todo config for a specific key"
24 | def get(key, default \\ nil) when is_atom(key) do
25 | get() |> Keyword.get(key, default)
26 | end
27 |
28 |
29 | @doc "Return the Application mode"
30 | def mode do
31 | mode =
32 | :env_var
33 | |> get()
34 | |> System.get_env
35 | |> normalize()
36 | |> fallback_to_default()
37 |
38 | case (mode in allowed_modes()) do
39 | true -> mode
40 | false -> raise "Unknown Application Mode"
41 | end
42 | end
43 |
44 |
45 | @doc "Return the correct adapter for config"
46 | def adapter do
47 | get(:adapters) |> Keyword.get(mode())
48 | end
49 |
50 |
51 |
52 |
53 |
54 | ## Private Helpers
55 | ## ---------------
56 |
57 |
58 | defp allowed_modes do
59 | Keyword.keys(get(:adapters))
60 | end
61 |
62 | defp normalize(str) do
63 | "#{str}"
64 | |> String.downcase
65 | |> String.to_existing_atom
66 | end
67 |
68 | defp fallback_to_default(mode) do
69 | case mode do
70 | mode when mode in ["", :"", nil] ->
71 | get(:default)
72 |
73 | mode -> mode
74 | end
75 | end
76 |
77 | end
78 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Mixfile do
2 | use Mix.Project
3 |
4 |
5 | def project do
6 | [
7 | app: :ztd,
8 | version: "0.0.1",
9 | elixir: "~> 1.4",
10 | elixirc_paths: elixirc_paths(Mix.env),
11 | compilers: [:phoenix, :gettext] ++ Mix.compilers,
12 | start_permanent: Mix.env == :prod,
13 | aliases: aliases(),
14 | deps: deps()
15 | ]
16 | end
17 |
18 |
19 | def application do
20 | [
21 | mod: {ZTD.Application, []},
22 | extra_applications: [:logger, :runtime_tools]
23 | ]
24 | end
25 |
26 |
27 | defp elixirc_paths(:test), do: ["lib", "test/support"]
28 | defp elixirc_paths(_), do: ["lib"]
29 |
30 |
31 | defp deps do
32 | [
33 | {:phoenix, "~> 1.3.2"},
34 | {:phoenix_pubsub, "~> 1.0"},
35 | {:phoenix_ecto, "~> 3.2"},
36 | {:postgrex, ">= 0.0.0"},
37 | {:gettext, "~> 0.11"},
38 | {:cowboy, "~> 1.0"},
39 | {:phoenix_html, "~> 2.10"},
40 | {:phoenix_live_reload, "~> 1.0", only: :dev},
41 |
42 | {:ecto_rut, "~> 1.2.2"},
43 | {:better_params, "~> 0.5.0"},
44 | {:react_phoenix, "~> 0.5.2"},
45 | {:amqp, "~> 0.3.1"},
46 | ]
47 | end
48 |
49 |
50 | defp aliases do
51 | [
52 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
53 | "ecto.reset": ["ecto.drop", "ecto.setup"],
54 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]
55 | ]
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/ztd/todo/supervisor.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Todo.Supervisor do
2 | use Supervisor
3 |
4 | alias ZTD.Todo.Config
5 | import Supervisor.Spec
6 |
7 |
8 | @moduledoc """
9 | Manages the Todo Supervision Tree and starts the
10 | appropriate Mode Adapter (Engine/Worker) depending
11 | on the specified env config
12 |
13 | NOTE:
14 | If the application grows too complex, consider
15 | creating sub-supervisors for both Engine and Worker
16 | within their Adapters
17 | """
18 |
19 |
20 |
21 |
22 | ## Public API
23 | ## ----------
24 |
25 |
26 | @doc "Start the Todo Supervisor"
27 | def start_link do
28 | Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
29 | end
30 |
31 |
32 |
33 |
34 |
35 | ## Callbacks
36 | ## ---------
37 |
38 |
39 | def init(:ok) do
40 | Config.mode()
41 | |> children()
42 | |> Supervisor.init(strategy: :one_for_one)
43 | end
44 |
45 |
46 |
47 |
48 |
49 | ## Children Spec
50 | ## -------------
51 |
52 |
53 | # Children for Engine Mode
54 | defp children(:engine) do
55 | [
56 | supervisor(ZTD.Repo, []),
57 | worker(ZTD.Todo.Engine.Broadcaster, []),
58 | worker(ZTD.Todo.Engine.Listener, []),
59 | ]
60 | end
61 |
62 |
63 | # Children for Worker Mode
64 | defp children(:worker) do
65 | [
66 | worker(ZTD.Todo.Worker.Listener, []),
67 | worker(ZTD.Todo.Worker.Dispatcher, []),
68 | ]
69 | end
70 |
71 |
72 |
73 | # Raise error for other modes
74 | defp children(_mode) do
75 | raise "Supervision Tree not defined for specified mode"
76 | end
77 |
78 |
79 | end
80 |
--------------------------------------------------------------------------------
/lib/ztd/todo/engine/engine.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Todo.Engine do
2 | alias ZTD.Repo
3 | alias ZTD.Todo.Event
4 | alias ZTD.Todo.Engine.Schema
5 | alias ZTD.Todo.Engine.Broadcaster
6 | alias Ecto.Query
7 |
8 | require Query
9 |
10 |
11 | @moduledoc """
12 | Engine implementation for the Todo interface.
13 | Implements all methods defined in Todo module.
14 | """
15 |
16 |
17 |
18 | # Public API
19 | # ----------
20 |
21 |
22 | @doc "Get all todos"
23 | def all do
24 | Schema
25 | |> Query.order_by(asc: :inserted_at)
26 | |> Repo.all
27 | end
28 |
29 |
30 |
31 | @doc "Insert new todo"
32 | def insert(%{} = params) do
33 | params
34 | |> Schema.insert
35 | |> broadcast!(:insert)
36 | end
37 |
38 |
39 |
40 | @doc "Update a todo"
41 | def update(id, %{} = params) do
42 | id
43 | |> Schema.get!
44 | |> Schema.update(params)
45 | |> broadcast!(:update)
46 | end
47 |
48 |
49 |
50 | @doc """
51 | Delete a todo
52 |
53 | Using a where clause so it doesn't raise errors for
54 | stale structs
55 | """
56 | def delete(id) do
57 | Schema
58 | |> Query.where([i], i.id == ^id)
59 | |> Repo.delete_all
60 |
61 | broadcast!({:ok, %{id: id}}, :delete)
62 | end
63 |
64 |
65 |
66 |
67 |
68 | # Private Helpers
69 | # ---------------
70 |
71 |
72 | # Broadcast on success
73 | defp broadcast!({:ok, item} = term, type) do
74 | type
75 | |> Event.new(item)
76 | |> Broadcaster.broadcast!
77 |
78 | term
79 | end
80 |
81 | defp broadcast!(term, _type) do
82 | term
83 | end
84 |
85 |
86 | end
87 |
--------------------------------------------------------------------------------
/test/support/support.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Tests.Support do
2 | @moduledoc "Support methods for tests"
3 |
4 |
5 | @doc """
6 | Setup Ecto for each test
7 |
8 | If the test case interacts with the database, it cannot be async. For this
9 | reason, every test runs inside a transaction which is reset at the beginning
10 | of the test unless the test case is marked as async.
11 | """
12 | def setup_ecto(tags) do
13 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ZTD.Repo)
14 | unless tags[:async] do
15 | Ecto.Adapters.SQL.Sandbox.mode(ZTD.Repo, {:shared, self()})
16 | end
17 | :ok
18 | end
19 |
20 |
21 | @doc "Sleep / wait for something"
22 | def wait(ms \\ 5) do
23 | Process.sleep(ms)
24 | end
25 |
26 |
27 |
28 |
29 | defmodule Schema do
30 | @doc """
31 | A helper that transform changeset errors to a map of messages.
32 |
33 | assert {:error, changeset} = Accounts.create_user(%{password: "short"})
34 | assert "password is too short" in errors_on(changeset).password
35 | assert %{password: ["password is too short"]} = errors_on(changeset)
36 |
37 | """
38 | def errors_on(changeset) do
39 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
40 | Enum.reduce(opts, message, fn {key, value}, acc ->
41 | String.replace(acc, "%{#{key}}", to_string(value))
42 | end)
43 | end)
44 | end
45 |
46 |
47 | def error_message(changeset, field) do
48 | case changeset.errors[field] do
49 | {message, _} -> message
50 | _something -> nil
51 | end
52 | end
53 |
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/ztd_web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Web.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | Enum.map(Keyword.get_values(form.errors, field), fn (error) ->
13 | content_tag :span, translate_error(error), class: "help-block"
14 | end)
15 | end
16 |
17 | @doc """
18 | Translates an error message using gettext.
19 | """
20 | def translate_error({msg, opts}) do
21 | # When using gettext, we typically pass the strings we want
22 | # to translate as a static argument:
23 | #
24 | # # Translate "is invalid" in the "errors" domain
25 | # dgettext "errors", "is invalid"
26 | #
27 | # # Translate the number of files with plural rules
28 | # dngettext "errors", "1 file", "%{count} files", count
29 | #
30 | # Because the error messages we show in our forms and APIs
31 | # are defined inside Ecto, we need to translate them dynamically.
32 | # This requires us to call the Gettext module passing our gettext
33 | # backend as first argument.
34 | #
35 | # Note we use the "errors" domain, which means translations
36 | # should be written to the errors.po file. The :count option is
37 | # set by Ecto and indicates we should also apply plural rules.
38 | if count = opts[:count] do
39 | Gettext.dngettext(ZTD.Web.Gettext, "errors", msg, msg, count, opts)
40 | else
41 | Gettext.dgettext(ZTD.Web.Gettext, "errors", msg, opts)
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test/ztd/todo/event_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Tests.Todo.Event do
2 | use ZTD.Tests.Support.Case, async: true
3 | alias ZTD.Todo.Event
4 |
5 |
6 |
7 | describe "new/2" do
8 | test "returns an Event struct for valid args" do
9 | assert %Event{type: :insert, data: %{}} = Event.new(:insert, %{})
10 | end
11 |
12 |
13 | test "raises error for invalid type" do
14 | assert_raise(Event.InvalidError, fn ->
15 | Event.new(:unknown_type, %{})
16 | end)
17 | end
18 | end
19 |
20 |
21 |
22 | describe "encode!/1" do
23 | setup do
24 | [event: Event.new(:insert, %{a: "X", b: "Y"})]
25 | end
26 |
27 | test "converts Event struct into json", %{event: event} do
28 | string = Event.encode!(event)
29 |
30 | assert string =~ ~r/"type":"insert"/
31 | assert string =~ ~r/"a":"X"/
32 | assert string =~ ~r/"b":"Y"/
33 | end
34 | end
35 |
36 |
37 |
38 | describe "decode!/1" do
39 | @string "1,2,3"
40 | test "raises error for invalidly formed json" do
41 | assert_raise(Event.InvalidError, fn ->
42 | Event.decode!(@string)
43 | end)
44 | end
45 |
46 |
47 | @string Event.encode!(%Event{type: :invalid, data: %{id: "123"}})
48 | test "raises error for invalid event json" do
49 | assert_raise(Event.InvalidError, fn ->
50 | Event.decode!(@string)
51 | end)
52 | end
53 |
54 |
55 | @string Event.encode!(%Event{type: :delete, data: %{id: "123"}})
56 | test "converts valid event json string back to event" do
57 | assert %{type: :delete, data: %{id: "123"}} = Event.decode!(@string)
58 | end
59 | end
60 |
61 | end
62 |
63 |
--------------------------------------------------------------------------------
/assets/css/app.scss:
--------------------------------------------------------------------------------
1 | /* This file is for your main application css. */
2 |
3 |
4 | /* Basic Layout
5 | * ************ */
6 |
7 | #ztd-app {
8 | max-width: 25em;
9 | margin: 1em auto;
10 | }
11 |
12 |
13 | header.header {
14 | margin-bottom: 12px;
15 |
16 | h1 {
17 | text-align: center;
18 | margin-bottom: 20px;
19 | }
20 | }
21 |
22 |
23 |
24 |
25 |
26 | /* Todo Components
27 | * *************** */
28 |
29 | .todo-app .app-status {
30 | border-bottom: 1px solid #e5e5e5;
31 | margin-bottom: 30px;
32 | padding-bottom: 10px;
33 | font-size: 10px;
34 | text-align: center;
35 | text-transform: uppercase;
36 |
37 | span:not(:last-child) {
38 | margin-right: 15px;
39 | }
40 | }
41 |
42 |
43 | .todo-app .new-item {
44 | input {
45 | font-size: 1.5em;
46 | width: 100%;
47 | }
48 | }
49 |
50 | .todo-app .item-list {
51 | padding: 1em 2em;
52 | }
53 |
54 |
55 | .todo-app .item-list .item {
56 | font-size: 1.4em;
57 |
58 | .content {
59 | display: inline-block;
60 | width: 82%;
61 | border: none;
62 |
63 | &.is-done {
64 | color: #ccc;
65 | text-decoration: line-through;
66 | }
67 | }
68 |
69 | .status {
70 | display: inline-block;
71 | transform: scale(1.2);
72 | margin-right: 0.5em !important;
73 | }
74 |
75 | .delete {
76 | float: right;
77 | text-decoration: none;
78 | color: #777;
79 | margin-top: 1px;
80 | cursor: pointer;
81 | }
82 | }
83 |
84 |
85 | .todo-app .wall-holder {
86 | position: relative;
87 |
88 | .wall.active {
89 | position: absolute;
90 | background-color: rgba(#000000, 0.1);
91 | top: 0;
92 | left: 0;
93 | height: 100%;
94 | width: 100%;
95 | cursor: not-allowed;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/assets/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 | // http://brunch.io/docs/config#-files-
9 | // joinTo: {
10 | // "js/app.js": /^js/,
11 | // "js/vendor.js": /^(?!js)/
12 | // }
13 | //
14 | // To change the order of concatenation of files, explicitly mention here
15 | // order: {
16 | // before: [
17 | // "vendor/js/jquery-2.1.1.js",
18 | // "vendor/js/bootstrap.min.js"
19 | // ]
20 | // }
21 | },
22 | stylesheets: {
23 | joinTo: "css/app.css"
24 | },
25 | templates: {
26 | joinTo: "js/app.js"
27 | }
28 | },
29 |
30 | conventions: {
31 | // This option sets where we should place non-css and non-js assets in.
32 | // By default, we set this to "/assets/static". Files in this directory
33 | // will be copied to `paths.public`, which is "priv/static" by default.
34 | assets: /^(static)/
35 | },
36 |
37 | // Phoenix paths configuration
38 | paths: {
39 | // Dependencies and current project directories to watch
40 | watched: ["static", "css", "js", "vendor"],
41 | // Where to compile files to
42 | public: "../priv/static"
43 | },
44 |
45 | // Configure your plugins
46 | plugins: {
47 | babel: {
48 | presets: ["env", "react"],
49 | // Do not use ES6 compiler in vendor code
50 | ignore: [/vendor/]
51 | },
52 | sass: {
53 | mode: "native"
54 | }
55 | },
56 |
57 | modules: {
58 | autoRequire: {
59 | "js/app.js": ["js/app"]
60 | }
61 | },
62 |
63 | npm: {
64 | enabled: true
65 | }
66 | };
67 |
--------------------------------------------------------------------------------
/lib/ztd_web/channels/todo_events.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Web.Channels.TodoEvents do
2 | use Phoenix.Channel
3 |
4 | alias ZTD.Todo
5 | alias ZTD.Todo.Event
6 | alias ZTD.Web.Endpoint
7 |
8 |
9 | @channel "todo_events"
10 | @relay "event"
11 |
12 |
13 |
14 |
15 |
16 | # Public API
17 | # ----------
18 |
19 |
20 | @doc """
21 | Broadcasts a Todo Event to all connected clients on the
22 | channel
23 |
24 | NOTE:
25 | This method is being called from the main Todo modules,
26 | which may cause weird issues in situations where the
27 | Endpoint process hasn't been started but this method is
28 | called. Consider wrapping this in another module which
29 | verifies the Endpoint process has already been started
30 | as part of the supervision tree.
31 | """
32 | def broadcast!(%Event{} = event) do
33 | Endpoint.broadcast!(@channel, @relay, event)
34 | end
35 |
36 |
37 |
38 |
39 |
40 | # Callbacks
41 | # ---------
42 |
43 |
44 | # Join Channel
45 |
46 | def join(@channel, _payload, socket) do
47 | {:ok, socket}
48 | end
49 |
50 |
51 |
52 | # Handle Events
53 |
54 | def handle_in("insert", payload, socket) do
55 | item = parse(payload)
56 | Todo.insert(item)
57 | {:noreply, socket}
58 | end
59 |
60 |
61 | def handle_in("update", payload, socket) do
62 | item = %{id: id} = parse(payload)
63 | Todo.update(id, item)
64 | {:noreply, socket}
65 | end
66 |
67 |
68 | def handle_in("delete", payload, socket) do
69 | item = %{id: id} = parse(payload)
70 | Todo.delete(id)
71 | {:noreply, socket}
72 | end
73 |
74 |
75 |
76 |
77 | # Private Helpers
78 | # ---------------
79 |
80 | defp parse(payload) do
81 | payload
82 | |> BetterParams.symbolize_merge(drop_string_keys: true)
83 | |> Map.get(:data)
84 | end
85 |
86 |
87 | end
88 |
--------------------------------------------------------------------------------
/lib/ztd_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Web.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :ztd
3 |
4 | socket "/socket", ZTD.Web.Sockets.Main
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: :ztd, 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.Logger
23 |
24 | plug Plug.Parsers,
25 | parsers: [:urlencoded, :multipart, :json],
26 | pass: ["*/*"],
27 | json_decoder: Poison
28 |
29 | plug Plug.MethodOverride
30 | plug Plug.Head
31 |
32 | # The session will be stored in the cookie and signed,
33 | # this means its contents can be read but not tampered with.
34 | # Set :encryption_salt if you would also like to encrypt it.
35 | plug Plug.Session,
36 | store: :cookie,
37 | key: "_ztd_key",
38 | signing_salt: "ekKX31ic"
39 |
40 | plug ZTD.Web.Router
41 |
42 | @doc """
43 | Callback invoked for dynamically configuring the endpoint.
44 |
45 | It receives the endpoint configuration and checks if
46 | configuration should be loaded from the system environment.
47 | """
48 | def init(_key, config) do
49 | if config[:load_from_system_env] do
50 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
51 | {:ok, Keyword.put(config, :http, [:inet6, port: port])}
52 | else
53 | {:ok, config}
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/ztd/todo/engine/broadcaster.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Todo.Engine.Broadcaster do
2 | use GenServer
3 |
4 | alias ZTD.Todo.Event
5 | alias ZTD.Todo.Config
6 | alias ZTD.Web.Channels
7 |
8 | @exchange Config.get(:amqp)[:broadcast_exchange]
9 | @routing Config.get(:amqp)[:broadcast_routing]
10 |
11 |
12 | @moduledoc """
13 | Keeps a RabbitMQ connection open to a fanout exchange
14 | where each worker is connected to. When messages are
15 | broadcasted, they're sent to all workers, and also to
16 | all the Phoenix channels (locally, on each instance).
17 | """
18 |
19 |
20 |
21 |
22 | ## Public API
23 | ## ----------
24 |
25 |
26 | @doc "Open the connection"
27 | def start_link do
28 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
29 | end
30 |
31 |
32 | @doc "Broadcast event on the fanout exchange"
33 | def broadcast!(%Event{} = event) do
34 | GenServer.cast(__MODULE__, {:broadcast, event})
35 | end
36 |
37 |
38 |
39 |
40 |
41 | ## Callbacks
42 | ## ---------
43 |
44 |
45 | # Initialize State
46 | @doc false
47 | def init(:ok) do
48 | # Create Connection & Channel
49 | {:ok, connection} = AMQP.Connection.open
50 | {:ok, channel} = AMQP.Channel.open(connection)
51 |
52 | # Declare Fanout Exchange
53 | AMQP.Exchange.declare(channel, @exchange, :fanout)
54 |
55 | {:ok, channel}
56 | end
57 |
58 |
59 |
60 | # Handle cast for :broadcast
61 | @doc false
62 | def handle_cast({:broadcast, event}, channel) do
63 | message = Event.encode!(event)
64 |
65 | # Broadcast on both Websocket and RabbitMQ
66 | Channels.TodoEvents.broadcast!(event)
67 | AMQP.Basic.publish(channel, @exchange, @routing, message)
68 |
69 | {:noreply, channel}
70 | end
71 |
72 |
73 |
74 | # Discard all info messages
75 | @doc false
76 | def handle_info(_message, state) do
77 | {:noreply, state}
78 | end
79 |
80 |
81 | end
82 |
--------------------------------------------------------------------------------
/lib/ztd/todo/worker/dispatcher.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Todo.Worker.Dispatcher do
2 | use GenServer
3 |
4 | alias ZTD.Todo.Event
5 | alias ZTD.Todo.Config
6 |
7 | @queue Config.get(:amqp)[:request_queue]
8 | @exchange Config.get(:amqp)[:request_exchange]
9 |
10 |
11 | @moduledoc """
12 | Keeps a RabbitMQ connection open to the engine. Sends
13 | todo events to the engine where it is responsible for
14 | persisting it and broadcasting the event to all
15 | connected workers (including self). This acts as an
16 | acknowledgement and the state is updated.
17 | """
18 |
19 |
20 |
21 |
22 |
23 | ## Public API
24 | ## ----------
25 |
26 |
27 | @doc "Open the connection"
28 | def start_link do
29 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
30 | end
31 |
32 |
33 | @doc "Send event to Engine"
34 | def send!(%Event{} = event) do
35 | GenServer.cast(__MODULE__, {:send, event})
36 | end
37 |
38 |
39 |
40 |
41 |
42 | ## Callbacks
43 | ## ---------
44 |
45 |
46 | # Initialize State
47 | @doc false
48 | def init(:ok) do
49 | # Create Connection & Channel
50 | {:ok, connection} = AMQP.Connection.open
51 | {:ok, channel} = AMQP.Channel.open(connection)
52 |
53 | # Declare Exchange & Queue
54 | AMQP.Exchange.declare(channel, @exchange, :direct)
55 | AMQP.Queue.declare(channel, @queue, durable: false)
56 | AMQP.Queue.bind(channel, @queue, @exchange, routing_key: @queue)
57 |
58 | {:ok, channel}
59 | end
60 |
61 |
62 |
63 | # Handle cast for :send!
64 | @doc false
65 | def handle_cast({:send, event}, channel) do
66 | message = Event.encode!(event)
67 | AMQP.Basic.publish(channel, @exchange, @queue, message, type: "event")
68 |
69 | {:noreply, channel}
70 | end
71 |
72 |
73 |
74 | # Discard all info messages
75 | @doc false
76 | def handle_info(_message, state) do
77 | {:noreply, state}
78 | end
79 |
80 |
81 | end
82 |
--------------------------------------------------------------------------------
/assets/js/components/todo/todo-item.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import _ from 'lodash'
4 |
5 |
6 | class TodoItem extends React.Component {
7 | constructor(props) {
8 | super(props);
9 |
10 | this.handleDone = this.handleDone.bind(this);
11 | this.handleEdit = this.handleEdit.bind(this);
12 | this.handleDelete = this.handleDelete.bind(this);
13 | }
14 |
15 |
16 | handleDone() {
17 | const {item} = this.props;
18 | const updated = _.merge(item, {done: !item.done});
19 | this.props.broadcast("update", item);
20 | }
21 |
22 | handleEdit(e) {
23 | const {item} = this.props;
24 | const updated = _.merge(item, {title: e.target.value});
25 | this.props.broadcast("update", item);
26 | }
27 |
28 | handleDelete() {
29 | const {item} = this.props;
30 | this.props.broadcast("delete", item);
31 | }
32 |
33 |
34 |
35 | render() {
36 | const {item} = this.props;
37 | const doneClass = item.done ? 'is-done' : '';
38 |
39 | return (
40 |
58 | );
59 | }
60 | }
61 |
62 |
63 |
64 | // Prop Specification
65 | TodoItem.propTypes = {
66 | broadcast: PropTypes.func.isRequired,
67 | item: PropTypes.shape({
68 | id: PropTypes.string.isRequired,
69 | title: PropTypes.string.isRequired,
70 | done: PropTypes.bool.isRequired,
71 | }).isRequired,
72 | };
73 |
74 |
75 |
76 | // Export
77 | export default TodoItem;
78 |
79 |
--------------------------------------------------------------------------------
/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 :ztd, ZTD.Web.Endpoint,
10 | http: [port: System.get_env("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 | cd: Path.expand("../assets", __DIR__)]]
16 |
17 | # ## SSL Support
18 | #
19 | # In order to use HTTPS in development, a self-signed
20 | # certificate can be generated by running the following
21 | # command from your terminal:
22 | #
23 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem
24 | #
25 | # The `http:` config above can be replaced with:
26 | #
27 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"],
28 | #
29 | # If desired, both `http:` and `https:` keys can be
30 | # configured to run both http and https servers on
31 | # different ports.
32 |
33 | # Watch static and templates for browser reloading.
34 | config :ztd, ZTD.Web.Endpoint,
35 | live_reload: [
36 | patterns: [
37 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
38 | ~r{priv/gettext/.*(po)$},
39 | ~r{lib/ztd_web/views/.*(ex)$},
40 | ~r{lib/ztd_web/templates/.*(eex)$}
41 | ]
42 | ]
43 |
44 | # Do not include metadata nor timestamps in development logs
45 | config :logger, :console, format: "[$level] $message\n"
46 |
47 | # Set a higher stacktrace during development. Avoid configuring such
48 | # in production as building large stacktraces may be expensive.
49 | config :phoenix, :stacktrace_depth, 20
50 |
51 | # Configure your database
52 | config :ztd, ZTD.Repo,
53 | adapter: Ecto.Adapters.Postgres,
54 | username: "postgres",
55 | password: "postgres",
56 | database: "ztd_dev",
57 | hostname: "localhost",
58 | pool_size: 10
59 |
--------------------------------------------------------------------------------
/lib/ztd_web.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Web do
2 | @moduledoc """
3 | The entrypoint for defining your web interface, such
4 | as controllers, views, channels and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use ZTD.Web, :controller
9 | use ZTD.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. Instead, define any helper function in modules
17 | and import those modules here.
18 | """
19 |
20 |
21 | def controller do
22 | quote do
23 | use Phoenix.Controller, namespace: ZTD.Web
24 | alias ZTD.Web.Router.Helpers, as: Router
25 |
26 | import Plug.Conn
27 | import ZTD.Web.Gettext
28 |
29 | plug :put_view, ZTD.Web.inflect_view(__MODULE__)
30 | plug :put_layout, {ZTD.Web.Views.Layout, :app}
31 | end
32 | end
33 |
34 |
35 | def view do
36 | quote do
37 | use Phoenix.HTML
38 | use Phoenix.View,
39 | root: "lib/ztd_web/templates",
40 | namespace: ZTD.Web.Views
41 |
42 | alias ZTD.Web.Router.Helpers, as: Router
43 | alias ReactPhoenix.ClientSide, as: React
44 |
45 | import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
46 | import ZTD.Web.ErrorHelpers
47 | import ZTD.Web.Gettext
48 | end
49 | end
50 |
51 |
52 | def router do
53 | quote do
54 | use Phoenix.Router
55 |
56 | import Plug.Conn
57 | import Phoenix.Controller
58 | end
59 | end
60 |
61 |
62 | def channel do
63 | quote do
64 | use Phoenix.Channel
65 | import ZTD.Web.Gettext
66 | end
67 | end
68 |
69 |
70 |
71 | @doc """
72 | When used, dispatch to the appropriate controller/view/etc.
73 | """
74 | defmacro __using__(which) when is_atom(which) do
75 | apply(__MODULE__, which, [])
76 | end
77 |
78 |
79 |
80 | # Figure out what View / Template to use from Controller Module
81 | def inflect_view(controller) do
82 | controller
83 | |> to_string
84 | |> String.replace("Controllers", "Views")
85 | |> String.to_atom
86 | end
87 |
88 | end
89 |
--------------------------------------------------------------------------------
/lib/ztd/todo/worker/rpc.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Todo.Worker.RPC do
2 | alias ZTD.Todo.Config
3 |
4 |
5 | @moduledoc """
6 | RPC Client interface to the engine. Sends rpc
7 | requests via RabbitMQ to the engine and waits
8 | for their response on an exclusive queue.
9 |
10 | NOTE:
11 | Highly inefficient over the network because it
12 | blocks until it receives response. Consider
13 | implementing a truly distributed version (but
14 | it would require each worker to have own
15 | state).
16 |
17 | Maybe also keep the connection/channel open.
18 | """
19 |
20 |
21 | @request_queue Config.get(:amqp)[:request_queue]
22 | @request_exchange Config.get(:amqp)[:request_exchange]
23 | @timeout 2_000
24 |
25 |
26 |
27 |
28 |
29 | ## Public API
30 | ## ----------
31 |
32 | def all do
33 | rpc_request!("all")
34 | end
35 |
36 |
37 |
38 |
39 |
40 | ## Private Helpers
41 | ## ---------------
42 |
43 |
44 | # Generate a random correlation id
45 | defp generate_id do
46 | :erlang.unique_integer
47 | |> :erlang.integer_to_binary
48 | |> Base.encode64
49 | end
50 |
51 |
52 | # Open a Connection
53 | defp open_connection do
54 | {:ok, connection} = AMQP.Connection.open
55 | {:ok, channel} = AMQP.Channel.open(connection)
56 | {:ok, %{queue: queue}} = AMQP.Queue.declare(channel, "", exclusive: true)
57 |
58 | {connection, channel, queue}
59 | end
60 |
61 |
62 | # Perform an RPC Request
63 | defp rpc_request!(command) do
64 | id = generate_id()
65 | conn = {_, channel, queue} = open_connection()
66 |
67 | AMQP.Basic.consume(channel, queue, nil, no_ack: true)
68 | AMQP.Basic.publish(
69 | channel,
70 | @request_exchange,
71 | @request_queue,
72 | command,
73 | type: "rpc",
74 | reply_to: queue,
75 | correlation_id: id
76 | )
77 |
78 | wait_for_response(conn, id)
79 | end
80 |
81 |
82 | # Block until receive RPC response with matching id
83 | defp wait_for_response(conn, id) do
84 | receive do
85 | {:basic_deliver, payload, %{correlation_id: ^id}} ->
86 | Poison.decode!(payload)
87 | after
88 | @timeout -> raise "RPC Call Timed out"
89 | end
90 | end
91 |
92 |
93 | end
94 |
95 |
--------------------------------------------------------------------------------
/test/ztd_web/channels/todo_events_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Tests.Web.Channels.TodoEvents do
2 | use ZTD.Tests.Support.ChannelCase
3 |
4 | alias ZTD.Todo.Engine.Schema, as: Item
5 | alias ZTD.Web.Channels.TodoEvents
6 |
7 |
8 | @channel TodoEvents
9 | @room "todo_events"
10 | @relay "event"
11 |
12 |
13 |
14 | setup do
15 | {:ok, _, socket} =
16 | "user_id"
17 | |> socket(%{})
18 | |> subscribe_and_join(@channel, @room)
19 |
20 | [socket: socket]
21 | end
22 |
23 |
24 |
25 | # NOTE:
26 | # Have separate tests for broadcasts
27 |
28 |
29 | describe "#handle_in" do
30 | @event "insert"
31 | test "inserts new items on insert event", %{socket: socket} do
32 | assert length(Item.all) == 0
33 |
34 | new = %{title: "get milk"}
35 | push(socket, @event, %{data: new})
36 | assert_broadcast!(@event, %{data: %{title: new.title}})
37 | Support.wait
38 |
39 | assert [item] = Item.all
40 | assert item.title == new.title
41 | end
42 |
43 |
44 | @event "update"
45 | test "updates existing items", %{socket: socket} do
46 | old = Item.insert!(title: "done item", done: true)
47 | new = %{id: old.id, title: "pending", done: false}
48 |
49 | push(socket, @event, %{data: new})
50 | assert_broadcast!(@event, %{data: %{title: new.title}})
51 | Support.wait
52 |
53 | assert item = Item.get(old.id)
54 | assert item.title == new.title
55 | assert item.done == new.done
56 | end
57 |
58 |
59 | @event "delete"
60 | test "deletes existing items", %{socket: socket} do
61 | item_1 = Item.insert!(title: "done item", done: true)
62 | item_2 = Item.insert!(title: "pending item", done: false)
63 |
64 | push(socket, @event, %{data: %{id: item_1.id}})
65 | assert_broadcast!(@event, %{data: %{id: item_1.id}})
66 | Support.wait
67 |
68 | refute Item.get(item_1.id)
69 | assert Item.get(item_2.id)
70 | end
71 | end
72 |
73 |
74 |
75 |
76 | # Private Helpers
77 | # ---------------
78 |
79 |
80 | # NOTE:
81 | # There is something weird happening here. Fix it.
82 | defp assert_broadcast!(event, payload) do
83 | payload = Map.put(payload, :type, String.to_atom(event))
84 | assert_broadcast(@relay, payload)
85 | end
86 |
87 | end
88 |
--------------------------------------------------------------------------------
/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, we often load configuration from external
4 | # sources, such as your system environment. For this reason,
5 | # you won't find the :http configuration below, but set inside
6 | # ZTD.Web.Endpoint.init/2 when load_from_system_env is
7 | # true. Any dynamic configuration should be done there.
8 | #
9 | # Don't forget to configure the url host to something meaningful,
10 | # Phoenix uses this information when generating URLs.
11 | #
12 | # Finally, we also include the path to a cache manifest
13 | # containing the digested version of static files. This
14 | # manifest is generated by the mix phx.digest task
15 | # which you typically run after static files are built.
16 | config :ztd, ZTD.Web.Endpoint,
17 | load_from_system_env: true,
18 | url: [host: "example.com", port: 80],
19 | cache_static_manifest: "priv/static/cache_manifest.json"
20 |
21 | # Do not print debug messages in production
22 | config :logger, level: :info
23 |
24 | # ## SSL Support
25 | #
26 | # To get SSL working, you will need to add the `https` key
27 | # to the previous section and set your `:url` port to 443:
28 | #
29 | # config :ztd, ZTD.Web.Endpoint,
30 | # ...
31 | # url: [host: "example.com", port: 443],
32 | # https: [:inet6,
33 | # port: 443,
34 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
35 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
36 | #
37 | # Where those two env variables return an absolute path to
38 | # the key and cert in disk or a relative path inside priv,
39 | # for example "priv/ssl/server.key".
40 | #
41 | # We also recommend setting `force_ssl`, ensuring no data is
42 | # ever sent via http, always redirecting to https:
43 | #
44 | # config :ztd, ZTD.Web.Endpoint,
45 | # force_ssl: [hsts: true]
46 | #
47 | # Check `Plug.SSL` for all available options in `force_ssl`.
48 |
49 | # ## Using releases
50 | #
51 | # If you are doing OTP releases, you need to instruct Phoenix
52 | # to start the server for all endpoints:
53 | #
54 | # config :phoenix, :serve_endpoints, true
55 | #
56 | # Alternatively, you can configure exactly which server to
57 | # start per endpoint:
58 | #
59 | # config :ztd, ZTD.Web.Endpoint, server: true
60 | #
61 |
62 | # Finally import the config/prod.secret.exs
63 | # which should be versioned separately.
64 | import_config "prod.secret.exs"
65 |
--------------------------------------------------------------------------------
/assets/js/socket.js:
--------------------------------------------------------------------------------
1 | // NOTE: The contents of this file will only be executed if
2 | // you uncomment its entry in "assets/js/app.js".
3 |
4 | // To use Phoenix channels, the first step is to import Socket
5 | // and connect at the socket path in "lib/web/endpoint.ex":
6 | import {Socket} from "phoenix"
7 |
8 | let socket = new Socket("/socket", {params: {token: window.userToken}})
9 |
10 | // When you connect, you'll often need to authenticate the client.
11 | // For example, imagine you have an authentication plug, `MyAuth`,
12 | // which authenticates the session and assigns a `:current_user`.
13 | // If the current user exists you can assign the user's token in
14 | // the connection for use in the layout.
15 | //
16 | // In your "lib/web/router.ex":
17 | //
18 | // pipeline :browser do
19 | // ...
20 | // plug MyAuth
21 | // plug :put_user_token
22 | // end
23 | //
24 | // defp put_user_token(conn, _) do
25 | // if current_user = conn.assigns[:current_user] do
26 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id)
27 | // assign(conn, :user_token, token)
28 | // else
29 | // conn
30 | // end
31 | // end
32 | //
33 | // Now you need to pass this token to JavaScript. You can do so
34 | // inside a script tag in "lib/web/templates/layout/app.html.eex":
35 | //
36 | //
37 | //
38 | // You will need to verify the user token in the "connect/2" function
39 | // in "lib/web/channels/user_socket.ex":
40 | //
41 | // def connect(%{"token" => token}, socket) do
42 | // # max_age: 1209600 is equivalent to two weeks in seconds
43 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
44 | // {:ok, user_id} ->
45 | // {:ok, assign(socket, :user, user_id)}
46 | // {:error, reason} ->
47 | // :error
48 | // end
49 | // end
50 | //
51 | // Finally, pass the token on connect as below. Or remove it
52 | // from connect if you don't care about authentication.
53 |
54 | socket.connect()
55 |
56 | // Now that you are connected, you can join channels with a topic:
57 | // let channel = socket.channel("topic:subtopic", {})
58 | // channel.join()
59 | // .receive("ok", resp => { console.log("Joined successfully", resp) })
60 | // .receive("error", resp => { console.log("Unable to join", resp) })
61 |
62 | export default socket
63 |
--------------------------------------------------------------------------------
/test/ztd/todo/engine_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Tests.Todo.Engine do
2 | use ZTD.Tests.Support.Case
3 |
4 | alias ZTD.Todo.Engine
5 |
6 |
7 |
8 | describe "all/0" do
9 | test "returns empty list when no todo items exist" do
10 | assert Engine.all == []
11 | end
12 |
13 |
14 | test "returns items in ascending order of creation date" do
15 | Engine.Schema.insert!(title: "Done 1", done: true)
16 | Engine.Schema.insert!(title: "Pending 1", done: false)
17 | Engine.Schema.insert!(title: "Pending 2", done: false)
18 | Engine.Schema.insert!(title: "Done 2", done: true)
19 |
20 | items = Engine.all |> Enum.map(&(&1.title))
21 | assert items == ["Done 1", "Pending 1", "Pending 2", "Done 2"]
22 | end
23 | end
24 |
25 |
26 |
27 | describe "insert/1" do
28 | test "returns :ok for valid params" do
29 | assert {:ok, _} = Engine.insert(%{title: "Buy Milk"})
30 | assert 1 = length(Engine.Schema.all)
31 |
32 | assert {:ok, _} = Engine.insert(%{title: "Buy Eggs", done: true})
33 | assert 2 = length(Engine.Schema.all)
34 | end
35 |
36 |
37 | test "return :error for invalid params" do
38 | assert {:error, changeset} = Engine.insert(%{title: nil})
39 | assert error_message(changeset, :title) =~ ~r/can't be blank/
40 | end
41 | end
42 |
43 |
44 |
45 | describe "update/2" do
46 | setup do
47 | [item: Engine.Schema.insert!(title: "Some Engine.Schema")]
48 | end
49 |
50 |
51 | test "can update the title of an item", %{item: item} do
52 | assert {:ok, item} = Engine.update(item.id, %{title: "New Name"})
53 | assert item.title == "New Name"
54 | end
55 |
56 |
57 | test "can mark items done", %{item: item} do
58 | assert {:ok, item} = Engine.update(item.id, %{done: true})
59 | assert item.done == true
60 | end
61 | end
62 |
63 |
64 |
65 | describe "delete/1" do
66 | setup do
67 | [item: Engine.Schema.insert!(title: "Some Engine.Schema")]
68 | end
69 |
70 |
71 | test "deletes an item if it exists", %{item: item} do
72 | assert {:ok, _} = Engine.delete(item.id)
73 | assert 0 = length(Engine.Schema.all)
74 | end
75 |
76 |
77 | test "does nothing if the item does not exist" do
78 | unknown_id = Ecto.UUID.generate
79 | assert {:ok, _} = Engine.delete(unknown_id)
80 | assert 1 = length(Engine.Schema.all)
81 | end
82 | end
83 |
84 | end
85 |
--------------------------------------------------------------------------------
/lib/ztd/todo/worker/listener.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Todo.Worker.Listener do
2 | use GenServer
3 | require Logger
4 |
5 | alias ZTD.Todo.Event
6 | alias ZTD.Todo.Config
7 | alias ZTD.Web.Channels
8 |
9 | @exchange Config.get(:amqp)[:broadcast_exchange]
10 | @routing Config.get(:amqp)[:broadcast_routing]
11 | @queue ""
12 |
13 |
14 | @moduledoc """
15 | Creates a temporary queue bound to the fanout exchange,
16 | listening for events broadcasted from the engine. When
17 | messages are received they are then broadcasted to the
18 | clients connected on the Phoenix Channel as well.
19 | """
20 |
21 |
22 |
23 |
24 | ## Public API
25 | ## ----------
26 |
27 |
28 | @doc "Open the connection"
29 | def start_link do
30 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
31 | end
32 |
33 |
34 |
35 |
36 |
37 | ## Callbacks
38 | ## ---------
39 |
40 |
41 | # Initialize State
42 | @doc false
43 | def init(:ok) do
44 | # Create Connection & Channel
45 | {:ok, connection} = AMQP.Connection.open
46 | {:ok, channel} = AMQP.Channel.open(connection)
47 |
48 | # Declare Exchange & Queue
49 | AMQP.Exchange.declare(channel, @exchange, :fanout)
50 | {:ok, %{queue: queue}} = AMQP.Queue.declare(channel, @queue, exclusive: true)
51 | AMQP.Queue.bind(channel, @queue, @exchange, routing_key: @routing)
52 |
53 | # Start Consuming
54 | AMQP.Basic.consume(channel, queue, nil, no_ack: true)
55 |
56 | {:ok, channel}
57 | end
58 |
59 |
60 |
61 | # Receive Messages
62 | @doc false
63 | def handle_info({:basic_deliver, payload, meta}, channel) do
64 | Logger.debug("Received Message: #{inspect payload}")
65 |
66 | spawn fn ->
67 | consume(payload, meta)
68 | end
69 |
70 | {:noreply, channel}
71 | end
72 |
73 |
74 |
75 | # Discard all other messages
76 | @doc false
77 | def handle_info(message, state) do
78 | Logger.debug("Received info: #{inspect message}")
79 | {:noreply, state}
80 | end
81 |
82 |
83 |
84 |
85 |
86 |
87 | ## Private Helpers
88 | ## ---------------
89 |
90 |
91 | # Parse the event and perform the actions
92 | defp consume(payload, _meta) do
93 | payload
94 | |> Event.decode!
95 | |> Channels.TodoEvents.broadcast!
96 | rescue
97 | Event.InvalidError ->
98 | raise "Received invalid event data #{inspect(payload)}"
99 | end
100 |
101 | end
102 |
103 |
--------------------------------------------------------------------------------
/test/ztd/todo/config_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Tests.Todo.Config do
2 | use ZTD.Tests.Support.Case
3 | alias ZTD.Todo.Config
4 |
5 |
6 | setup do
7 | configs = Application.get_env(:ztd, :todo)
8 |
9 | on_exit fn ->
10 | # Revert to default mode
11 | set_mode(configs[:default])
12 | end
13 |
14 | [configs: configs]
15 | end
16 |
17 |
18 |
19 | describe "get/0" do
20 | test "returns application config specified under :todo key", %{configs: configs} do
21 | assert Config.get == configs
22 | end
23 | end
24 |
25 |
26 |
27 | describe "get/2" do
28 | @key :env_var
29 | test "returns the nested config for given key", %{configs: configs} do
30 | assert Config.get(@key) == configs[@key]
31 | end
32 |
33 |
34 | @key :unknown_key
35 | test "returns the default value when nothing specified" do
36 | refute Config.get(@key)
37 | assert Config.get(@key, :default) == :default
38 | end
39 | end
40 |
41 |
42 |
43 | describe "mode/0" do
44 | test "returns default mode when not specified", %{configs: configs} do
45 | set_mode(nil)
46 | assert Config.mode == configs[:default]
47 | end
48 |
49 |
50 | test "returns the mode when specified" do
51 | set_mode(:engine)
52 | assert Config.mode == :engine
53 |
54 | set_mode(:worker)
55 | assert Config.mode == :worker
56 | end
57 |
58 |
59 | test "raises error for invalid modes" do
60 | assert_raise(RuntimeError, ~r/unknown application mode/i, fn ->
61 | set_mode(:invalid_mode)
62 | Config.mode
63 | end)
64 | end
65 | end
66 |
67 |
68 |
69 | describe "adapter/0" do
70 | test "returns the correct adapter when mode is specified", %{configs: configs} do
71 | set_mode(:engine)
72 | assert Config.adapter == configs[:adapters][:engine]
73 |
74 | set_mode(:worker)
75 | assert Config.adapter == configs[:adapters][:worker]
76 | end
77 |
78 |
79 | test "raises error for invalid modes" do
80 | assert_raise(RuntimeError, fn ->
81 | set_mode(:invalid_mode)
82 | Config.adapter
83 | end)
84 | end
85 | end
86 |
87 |
88 |
89 |
90 | # Private Helpers
91 | # ---------------
92 |
93 | defp set_mode(mode) do
94 | mode = String.upcase("#{mode}")
95 |
96 | :env_var
97 | |> Config.get
98 | |> System.put_env(mode)
99 | end
100 |
101 |
102 | end
103 |
--------------------------------------------------------------------------------
/priv/gettext/en/LC_MESSAGES/errors.po:
--------------------------------------------------------------------------------
1 | ## `msgid`s in this file come from POT (.pot) files.
2 | ##
3 | ## Do not add, change, or remove `msgid`s manually here as
4 | ## they're tied to the ones in the corresponding POT file
5 | ## (with the same domain).
6 | ##
7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge`
8 | ## to merge POT files into PO files.
9 | msgid ""
10 | msgstr ""
11 | "Language: en\n"
12 |
13 | ## From Ecto.Changeset.cast/4
14 | msgid "can't be blank"
15 | msgstr ""
16 |
17 | ## From Ecto.Changeset.unique_constraint/3
18 | msgid "has already been taken"
19 | msgstr ""
20 |
21 | ## From Ecto.Changeset.put_change/3
22 | msgid "is invalid"
23 | msgstr ""
24 |
25 | ## From Ecto.Changeset.validate_acceptance/3
26 | msgid "must be accepted"
27 | msgstr ""
28 |
29 | ## From Ecto.Changeset.validate_format/3
30 | msgid "has invalid format"
31 | msgstr ""
32 |
33 | ## From Ecto.Changeset.validate_subset/3
34 | msgid "has an invalid entry"
35 | msgstr ""
36 |
37 | ## From Ecto.Changeset.validate_exclusion/3
38 | msgid "is reserved"
39 | msgstr ""
40 |
41 | ## From Ecto.Changeset.validate_confirmation/3
42 | msgid "does not match confirmation"
43 | msgstr ""
44 |
45 | ## From Ecto.Changeset.no_assoc_constraint/3
46 | msgid "is still associated with this entry"
47 | msgstr ""
48 |
49 | msgid "are still associated with this entry"
50 | msgstr ""
51 |
52 | ## From Ecto.Changeset.validate_length/3
53 | msgid "should be %{count} character(s)"
54 | msgid_plural "should be %{count} character(s)"
55 | msgstr[0] ""
56 | msgstr[1] ""
57 |
58 | msgid "should have %{count} item(s)"
59 | msgid_plural "should have %{count} item(s)"
60 | msgstr[0] ""
61 | msgstr[1] ""
62 |
63 | msgid "should be at least %{count} character(s)"
64 | msgid_plural "should be at least %{count} character(s)"
65 | msgstr[0] ""
66 | msgstr[1] ""
67 |
68 | msgid "should have at least %{count} item(s)"
69 | msgid_plural "should have at least %{count} item(s)"
70 | msgstr[0] ""
71 | msgstr[1] ""
72 |
73 | msgid "should be at most %{count} character(s)"
74 | msgid_plural "should be at most %{count} character(s)"
75 | msgstr[0] ""
76 | msgstr[1] ""
77 |
78 | msgid "should have at most %{count} item(s)"
79 | msgid_plural "should have at most %{count} item(s)"
80 | msgstr[0] ""
81 | msgstr[1] ""
82 |
83 | ## From Ecto.Changeset.validate_number/3
84 | msgid "must be less than %{number}"
85 | msgstr ""
86 |
87 | msgid "must be greater than %{number}"
88 | msgstr ""
89 |
90 | msgid "must be less than or equal to %{number}"
91 | msgstr ""
92 |
93 | msgid "must be greater than or equal to %{number}"
94 | msgstr ""
95 |
96 | msgid "must be equal to %{number}"
97 | msgstr ""
98 |
--------------------------------------------------------------------------------
/priv/gettext/errors.pot:
--------------------------------------------------------------------------------
1 | ## This file is a PO Template file.
2 | ##
3 | ## `msgid`s here are often extracted from source code.
4 | ## Add new translations manually only if they're dynamic
5 | ## translations that can't be statically extracted.
6 | ##
7 | ## Run `mix gettext.extract` to bring this file up to
8 | ## date. Leave `msgstr`s empty as changing them here as no
9 | ## effect: edit them in PO (`.po`) files instead.
10 |
11 | ## From Ecto.Changeset.cast/4
12 | msgid "can't be blank"
13 | msgstr ""
14 |
15 | ## From Ecto.Changeset.unique_constraint/3
16 | msgid "has already been taken"
17 | msgstr ""
18 |
19 | ## From Ecto.Changeset.put_change/3
20 | msgid "is invalid"
21 | msgstr ""
22 |
23 | ## From Ecto.Changeset.validate_acceptance/3
24 | msgid "must be accepted"
25 | msgstr ""
26 |
27 | ## From Ecto.Changeset.validate_format/3
28 | msgid "has invalid format"
29 | msgstr ""
30 |
31 | ## From Ecto.Changeset.validate_subset/3
32 | msgid "has an invalid entry"
33 | msgstr ""
34 |
35 | ## From Ecto.Changeset.validate_exclusion/3
36 | msgid "is reserved"
37 | msgstr ""
38 |
39 | ## From Ecto.Changeset.validate_confirmation/3
40 | msgid "does not match confirmation"
41 | msgstr ""
42 |
43 | ## From Ecto.Changeset.no_assoc_constraint/3
44 | msgid "is still associated with this entry"
45 | msgstr ""
46 |
47 | msgid "are still associated with this entry"
48 | msgstr ""
49 |
50 | ## From Ecto.Changeset.validate_length/3
51 | msgid "should be %{count} character(s)"
52 | msgid_plural "should be %{count} character(s)"
53 | msgstr[0] ""
54 | msgstr[1] ""
55 |
56 | msgid "should have %{count} item(s)"
57 | msgid_plural "should have %{count} item(s)"
58 | msgstr[0] ""
59 | msgstr[1] ""
60 |
61 | msgid "should be at least %{count} character(s)"
62 | msgid_plural "should be at least %{count} character(s)"
63 | msgstr[0] ""
64 | msgstr[1] ""
65 |
66 | msgid "should have at least %{count} item(s)"
67 | msgid_plural "should have at least %{count} item(s)"
68 | msgstr[0] ""
69 | msgstr[1] ""
70 |
71 | msgid "should be at most %{count} character(s)"
72 | msgid_plural "should be at most %{count} character(s)"
73 | msgstr[0] ""
74 | msgstr[1] ""
75 |
76 | msgid "should have at most %{count} item(s)"
77 | msgid_plural "should have at most %{count} item(s)"
78 | msgstr[0] ""
79 | msgstr[1] ""
80 |
81 | ## From Ecto.Changeset.validate_number/3
82 | msgid "must be less than %{number}"
83 | msgstr ""
84 |
85 | msgid "must be greater than %{number}"
86 | msgstr ""
87 |
88 | msgid "must be less than or equal to %{number}"
89 | msgstr ""
90 |
91 | msgid "must be greater than or equal to %{number}"
92 | msgstr ""
93 |
94 | msgid "must be equal to %{number}"
95 | msgstr ""
96 |
--------------------------------------------------------------------------------
/lib/ztd/todo/engine/listener.ex:
--------------------------------------------------------------------------------
1 | defmodule ZTD.Todo.Engine.Listener do
2 | use GenServer
3 | require Logger
4 |
5 | alias ZTD.Todo
6 | alias ZTD.Todo.Event
7 | alias ZTD.Todo.Config
8 |
9 |
10 | @moduledoc """
11 | Listens to events directly sent from workers to the
12 | engine. Performs the operations associated with the
13 | events using the main Engine module, which on success,
14 | broadcast them back to all workers. This acts as an
15 | acknowledgement, updating the workers Web UI state.
16 |
17 | NOTE:
18 | This is inefficient. For making this app "truly"
19 | distributed, consider having atleast a volatile state
20 | for each worker (maybe as a process?). This way we
21 | can reflect changes in UI instantly, and eventually
22 | support CQRS for partition tolerance.
23 |
24 | Also create a separate GenServer for handling RPC
25 | requests.
26 | """
27 |
28 |
29 | @queue Config.get(:amqp)[:request_queue]
30 | @exchange Config.get(:amqp)[:request_exchange]
31 | @rpc ""
32 |
33 |
34 |
35 |
36 | ## Public API
37 | ## ----------
38 |
39 |
40 | @doc "Open the connection"
41 | def start_link do
42 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
43 | end
44 |
45 |
46 |
47 |
48 | ## Callbacks
49 | ## ---------
50 |
51 |
52 | # Initialize State
53 | @doc false
54 | def init(:ok) do
55 | # Create Connection & Channel
56 | {:ok, connection} = AMQP.Connection.open
57 | {:ok, channel} = AMQP.Channel.open(connection)
58 |
59 | # Declare Exchange & Queue
60 | AMQP.Exchange.declare(channel, @exchange, :direct)
61 | AMQP.Queue.declare(channel, @queue, durable: false)
62 | AMQP.Queue.bind(channel, @queue, @exchange, routing_key: @queue)
63 |
64 | # Start Consuming
65 | AMQP.Basic.consume(channel, @queue, nil, no_ack: true)
66 |
67 | {:ok, channel}
68 | end
69 |
70 |
71 |
72 | # Receive Messages
73 | @doc false
74 | def handle_info({:basic_deliver, payload, meta}, channel) do
75 | Logger.debug("Received Payload: #{inspect payload}")
76 |
77 | spawn fn ->
78 | consume(channel, payload, meta)
79 | end
80 |
81 | {:noreply, channel}
82 | end
83 |
84 |
85 |
86 | # Discard all other messages
87 | @doc false
88 | def handle_info(message, state) do
89 | Logger.debug("Received info: #{inspect message}")
90 | {:noreply, state}
91 | end
92 |
93 |
94 |
95 |
96 |
97 | ## Private Helpers
98 | ## ---------------
99 |
100 |
101 | # Consume RPC calls and respond accordingly
102 | defp consume(channel, command, %{type: "rpc"} = meta) do
103 | Logger.debug("Message Type: RPC Call")
104 |
105 | case command do
106 | "all" ->
107 | payload = Poison.encode!(Todo.all)
108 | rpc_reply!(channel, meta, payload)
109 |
110 | _ ->
111 | raise "Unknown RPC Command: #{inspect(command)}"
112 | end
113 | end
114 |
115 |
116 | # Consume event messages and perform appropriate actions
117 | defp consume(_channel, payload, %{type: "event"}) do
118 | Logger.debug("Message Type: Event Dispatch")
119 | %Event{type: type, data: data} = Event.decode!(payload)
120 |
121 | case type do
122 | :insert ->
123 | Todo.insert(data)
124 |
125 | :update ->
126 | Todo.update(data.id, data)
127 |
128 | :delete ->
129 | Todo.delete(data.id)
130 |
131 | _ ->
132 | raise "Don't know how to perform operation #{inspect(type)}"
133 | end
134 |
135 | rescue
136 | Event.InvalidError ->
137 | raise "Received invalid event data #{inspect(payload)}"
138 | end
139 |
140 |
141 | # Handle unknown message types
142 | defp consume(_channel, _payload, meta) do
143 | Logger.error("Unknown Message Type. Supplied Metadata: #{inspect(meta)}")
144 | end
145 |
146 |
147 | # Respond to an RPC call
148 | defp rpc_reply!(channel, meta, response) do
149 | AMQP.Basic.publish(
150 | channel,
151 | @rpc,
152 | meta.reply_to,
153 | response,
154 | correlation_id: meta.correlation_id
155 | )
156 | rescue
157 | _ -> raise "Could not reply to RPC request: #{inspect(meta)}"
158 | end
159 |
160 |
161 | end
162 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ZTD
2 | ===
3 |
4 | [![Build Status][shield-travis]][travis-ci]
5 | [![License][shield-license]][github]
6 |
7 | > Todo App written in Elixir/Phoenix using RabbitMQ
8 |
9 | This is a small experiment using RabbitMQ. The app is designed to be run in
10 | one of two modes; `Engine` and `Worker`. When run in Engine mode, the app
11 | stores and reads items from disk. When run in Worker mode, the app doesn't
12 | have it's own state, instead it listens to events from the engine and
13 | dispatches events to it when actions are performed.
14 |
15 | **Demo:**
16 |
17 | [![ZTD Demo Video][demo-thumb]][demo-video]
18 |
19 |
20 |
21 |
22 |
23 |
24 | ## Setup
25 |
26 | Following dependencies are required:
27 |
28 | - Erlang 20+
29 | - Elixir 1.6+
30 | - Postgres
31 | - RabbitMQ
32 |
33 | Compile Application and Assets:
34 |
35 | ```bash
36 | $ mix do deps.get, compile
37 | $ mix do ecto.create, ecto.migrate
38 | $ cd assets && npm install
39 | ```
40 |
41 |
42 |
43 |
44 |
45 |
46 | ## Running the App
47 |
48 | To start the app in `Engine` mode:
49 |
50 | ```bash
51 | $ PORT=4000 APP_MODE=engine mix phx.server
52 | ```
53 |
54 | To start it in `Worker` mode:
55 |
56 | ```bash
57 | $ PORT=5000 APP_MODE=worker mix phx.server
58 | ```
59 |
60 | You can also use [`foreman`][foreman] to run multiple instances easily.
61 | This will start the engine on port `4000` and 4 workers on ports `5001`,
62 | `5002`, `5003` & `5004`:
63 |
64 | ```bash
65 | $ gem install foreman
66 | $ foreman start
67 | ```
68 |
69 |
70 |
71 |
72 |
73 |
74 | ## Implementation Details
75 |
76 | The application exports one main module `Todo`, whose implementation is
77 | further organized into two adapters `Engine` and `Worker`. Depending on
78 | the mode the application is started in, the supervisor only boots those
79 | processes, and the Todo module delegates the function calls to the
80 | appropriate adapter. Since the Web UI only directly interacts with the
81 | `Todo` module, so it does not concern itself with the implementation.
82 |
83 | When the application is running in the Engine mode, it stores and reads
84 | items from the Postgres database. Actions performed via the Todo
85 | interface are wrapped in an `Event` struct, and when successful, are
86 | broadcasted both on all open Phoenix channels as well as on a RabbitMQ
87 | fanout exchange.
88 |
89 | When run in Worker mode, the app does not have its own state, volatile
90 | or non-volatile. This is by design. It gets initial list of items via
91 | an RPC call implmented over RabbitMQ and listens for events on the
92 | fanout exchange. When actions are performed, they are again wrapped as
93 | events and dispatched to the Engine where when successfully performed,
94 | are broadcasted and received backed by all workers. This also acts as
95 | an acknowledgement.
96 |
97 | Admittedly, the app is not "truly distributed" since each worker does
98 | not have its own state, and does not have any kind of partition
99 | tolerance. Other than that, something like CQRS would be a great way
100 | to ensure consistency.
101 |
102 |
103 |
104 |
105 |
106 |
107 | ## Testing and Contributing
108 |
109 | The app consists of a very modest test suite. You can run them with mix:
110 |
111 | ```bash
112 | $ mix test
113 | ```
114 |
115 | You can also help by contributing to the project:
116 |
117 | - [Fork][github-fork], Enhance, Send PR
118 | - Lock issues with any bugs or feature requests
119 | - Implement something from Roadmap
120 | - Spread the word :heart:
121 |
122 |
123 |
124 |
125 |
126 |
127 | ## License
128 |
129 | The code is available as open source under the terms of the [MIT License][license].
130 |
131 |
132 |
133 |
134 |
135 |
136 | [shield-license]: https://img.shields.io/github/license/sheharyarn/ztd.svg
137 | [shield-travis]: https://img.shields.io/travis/sheharyarn/ztd/master.svg
138 | [travis-ci]: https://travis-ci.org/sheharyarn/ztd
139 |
140 | [license]: https://opensource.org/licenses/MIT
141 | [foreman]: https://github.com/ddollar/foreman
142 | [github]: https://github.com/sheharyarn/ztd/
143 | [github-fork]: https://github.com/sheharyarn/ztd/fork
144 |
145 | [demo-thumb]: https://i.imgur.com/wfYayRul.png
146 | [demo-video]: https://to.shyr.io/2tebNiP
147 |
--------------------------------------------------------------------------------
/assets/js/components/todo/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import _ from 'lodash'
4 |
5 | import Socket from '../../socket'
6 | import TodoNew from './todo-new'
7 | import TodoItem from './todo-item'
8 |
9 |
10 |
11 | class Todo extends React.Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | this.state = {
16 | status: 'inactive',
17 | items: this.props.items,
18 | };
19 |
20 | this.broadcast = this.broadcast.bind(this);
21 | this.handleOkEvent = this.handleOkEvent.bind(this);
22 | this.handleErrorEvent = this.handleErrorEvent.bind(this);
23 | }
24 |
25 |
26 | // Connect to channel on mount
27 | componentDidMount() {
28 | let channel = Socket.channel("todo_events", {});
29 | channel.on("event", this.handleOkEvent);
30 | channel.onError(() => this.setState({status: 'failed'}));
31 | channel.onClose(() => this.setState({status: 'inactive'}));
32 |
33 | channel
34 | .join()
35 | .receive("ok", () => this.setState({status: 'connected'}))
36 | .receive("error", () => this.setState({status: 'failed'}))
37 |
38 | this.setState({
39 | channel: channel,
40 | status: 'connecting',
41 | });
42 | }
43 |
44 |
45 | // Handle OK Response on Channels
46 | handleOkEvent(response) {
47 | const {items} = this.state;
48 | const item = response.data;
49 | let updated = items;
50 |
51 | switch (response.type) {
52 | // Insert new Item
53 | case "insert":
54 | updated = _.concat(items, item);
55 | console.log("Inserted:", item.id);
56 | break;
57 |
58 |
59 | // Find the item in the list
60 | case "update":
61 | updated = _.map(items, i => {
62 | if (i.id === item.id) {
63 | return _.merge(i, item);
64 | } else {
65 | return i;
66 | }
67 | });
68 | console.log("Updated: ", item.id);
69 | break;
70 |
71 |
72 | // Delete the item from list
73 | case "delete":
74 | updated = _.reject(items, i => {
75 | return i.id === item.id;
76 | });
77 | console.log("Deleted: ", item.id);
78 | break;
79 |
80 |
81 | // Unexpected Message
82 | default:
83 | console.log("Channel: ", response);
84 | updated = items;
85 | break;
86 | }
87 |
88 | // Finally update state
89 | this.setState({items: updated})
90 | }
91 |
92 |
93 | // Handle Error Response on Channels
94 | handleErrorEvent(response) {
95 | console.error("Error!", response);
96 | }
97 |
98 |
99 | // Broadcast item events on channel
100 | broadcast(type, item) {
101 | const {channel} = this.state;
102 |
103 | channel
104 | .push(type, {data: item})
105 | .receive("ok", this.handleOkEvent)
106 | .receive("error", this.handleErrorEvent)
107 | }
108 |
109 |
110 | renderStatus() {
111 | const {status} = this.state;
112 |
113 | switch (status) {
114 | case 'inactive': return "Disconnected";
115 | case 'connecting': return "Connecting...";
116 | case 'connected': return "Connected!";
117 | case 'failed': return "Connection Failed!";
118 | }
119 | }
120 |
121 |
122 | renderWall() {
123 | const {status} = this.state;
124 | const klass = (status === 'connected') ? 'wall inactive' : 'wall active';
125 |
126 | return (
);
127 | }
128 |
129 |
130 | render() {
131 | const {items} = this.state;
132 | const {mode} = this.props;
133 |
134 | return (
135 |
136 |
137 | App Mode: {mode}
138 | Socket: {this.renderStatus()}
139 |
140 |
141 |
142 |
145 |
146 |
147 | { items.map(i =>
148 |
153 | )}
154 |
155 |
156 | {this.renderWall()}
157 |
158 |
159 | );
160 | }
161 | }
162 |
163 |
164 |
165 | // Prop Specification
166 | Todo.defaultProps = {
167 | items: [],
168 | };
169 |
170 | Todo.propTypes = {
171 | mode: PropTypes.string.isRequired,
172 | items: PropTypes.arrayOf(TodoItem.propTypes.item).isRequired,
173 | };
174 |
175 |
176 |
177 | // Export
178 | export default Todo;
179 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "amqp": {:hex, :amqp, "0.3.1", "c22887ff851e57c8012a7b340a9a837ae997ee819079e447222100ef2a1a7735", [:mix], [{:amqp_client, "~> 3.6.14", [hex: :amqp_client, repo: "hexpm", optional: false]}, {:rabbit_common, "~> 3.6.14", [hex: :rabbit_common, repo: "hexpm", optional: false]}, {:recon, "~> 2.3.2", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm"},
3 | "amqp_client": {:hex, :amqp_client, "3.6.16", "9e61e7a021b850a3092c84b377fd6c0f19ae648e9b4cb7b41c620d4a9a397f3c", [:make, :rebar3], [{:rabbit_common, "3.6.16", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm"},
4 | "better_params": {:hex, :better_params, "0.5.0", "fc20b88a809cf34548392a2436b5974814b0f0abdd52ed4598db8ba21767d94f", [:mix], [{:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
5 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
6 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
7 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
8 | "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
9 | "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"},
10 | "ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
11 | "ecto_rut": {:hex, :ecto_rut, "1.2.2", "fba10d63ba2244a62d61e1d6a46e27e65dbf2d52a305a6309ec529b260b41fba", [:mix], [{:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_utils, "~> 0.1.4", [hex: :ex_utils, repo: "hexpm", optional: false]}], "hexpm"},
12 | "ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm"},
13 | "file_system": {:hex, :file_system, "0.2.5", "a3060f063b116daf56c044c273f65202e36f75ec42e678dc10653056d3366054", [:mix], [], "hexpm"},
14 | "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
15 | "goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "hexpm"},
16 | "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], [], "hexpm"},
17 | "lager": {:hex, :lager, "3.5.1", "63897a61af646c59bb928fee9756ce8bdd02d5a1a2f3551d4a5e38386c2cc071", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm"},
18 | "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
19 | "phoenix": {:hex, :phoenix, "1.3.2", "2a00d751f51670ea6bc3f2ba4e6eb27ecb8a2c71e7978d9cd3e5de5ccf7378bd", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
20 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.3.0", "702f6e164512853d29f9d20763493f2b3bcfcb44f118af2bc37bb95d0801b480", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
21 | "phoenix_html": {:hex, :phoenix_html, "2.11.2", "86ebd768258ba60a27f5578bec83095bdb93485d646fc4111db8844c316602d6", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
22 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.1.5", "8d4c9b1ef9ca82deee6deb5a038d6d8d7b34b9bb909d99784a49332e0d15b3dc", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
23 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"},
24 | "plug": {:hex, :plug, "1.5.1", "1ff35bdecfb616f1a2b1c935ab5e4c47303f866cb929d2a76f0541e553a58165", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.3", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
25 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
26 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"},
27 | "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
28 | "rabbit_common": {:hex, :rabbit_common, "3.6.16", "7f6a101963c151008b42373857ad7741be476210cfd4df5c4746ab45fa558861", [:make, :rebar3], [{:recon, "2.3.2", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm"},
29 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
30 | "ranch_proxy_protocol": {:hex, :ranch_proxy_protocol, "1.5.0", "e698aaeb590ad504b649dc0d3055abee6caf0b49d3caee1a080ae83b5b499f30", [:rebar3], [{:ranch, "1.5.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
31 | "react_phoenix": {:hex, :react_phoenix, "0.5.2", "53af6a8edb61f960258a2f5064af23a26354246dd049872ec77c9c6f12728aee", [:mix], [{:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
32 | "recon": {:hex, :recon, "2.3.2", "4444c879be323b1b133eec5241cb84bd3821ea194c740d75617e106be4744318", [:rebar3], [], "hexpm"},
33 | }
34 |
--------------------------------------------------------------------------------
/assets/css/phoenix.css:
--------------------------------------------------------------------------------
1 | /* Includes Bootstrap as well as some default style for the starter
2 | * application. This can be safely deleted to start fresh.
3 | */
4 |
5 | /*!
6 | * Bootstrap v3.3.5 (http://getbootstrap.com)
7 | * Copyright 2011-2015 Twitter, Inc.
8 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
9 | *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.33px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:3;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:2;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}
10 |
11 | /* Space out content a bit */
12 | body, form, ul, table {
13 | margin-top: 20px;
14 | margin-bottom: 20px;
15 | }
16 |
17 | /* Phoenix flash messages */
18 | .alert:empty { display: none; }
19 |
20 | /* Custom page header */
21 | .header {
22 | border-bottom: 1px solid #e5e5e5;
23 | }
24 | .logo {
25 | width: 519px;
26 | height: 71px;
27 | display: inline-block;
28 | margin-bottom: 1em;
29 | background-image: url("/images/phoenix.png");
30 | background-size: 519px 71px;
31 | }
32 |
33 | /* Everything but the jumbotron gets side spacing for mobile first views */
34 | .header,
35 | .marketing {
36 | padding-right: 15px;
37 | padding-left: 15px;
38 | }
39 |
40 | /* Customize container */
41 | @media (min-width: 768px) {
42 | .container {
43 | max-width: 730px;
44 | }
45 | }
46 | .container-narrow > hr {
47 | margin: 30px 0;
48 | }
49 |
50 | /* Main marketing message */
51 | .jumbotron {
52 | text-align: center;
53 | border-bottom: 1px solid #e5e5e5;
54 | }
55 |
56 | /* Supporting marketing content */
57 | .marketing {
58 | margin: 35px 0;
59 | }
60 |
61 | /* Responsive: Portrait tablets and up */
62 | @media screen and (min-width: 768px) {
63 | /* Remove the padding we set earlier */
64 | .header,
65 | .marketing {
66 | padding-right: 0;
67 | padding-left: 0;
68 | }
69 | /* Space out the masthead */
70 | .header {
71 | margin-bottom: 30px;
72 | }
73 | /* Remove the bottom border on the jumbotron for visual effect */
74 | .jumbotron {
75 | border-bottom: 0;
76 | }
77 | }
--------------------------------------------------------------------------------