├── .buildpacks
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── compile
├── config
├── config.exs
├── dev.exs
├── prod.exs
├── test.exs
└── travis.exs
├── elixir_buildpack.config
├── lib
├── phoenix_trello.ex
└── phoenix_trello
│ ├── board_channel
│ ├── monitor.ex
│ └── supervisor.ex
│ ├── endpoint.ex
│ ├── guardian_serializer.ex
│ ├── permalink.ex
│ └── repo.ex
├── mix.exs
├── mix.lock
├── package.json
├── phoenix_static_buildpack.config
├── priv
├── repo
│ ├── migrations
│ │ ├── 20151224075404_create_user.exs
│ │ ├── 20151224093233_create_board.exs
│ │ ├── 20151225091657_create_list.exs
│ │ ├── 20151227084131_create_card.exs
│ │ ├── 20151230081546_create_user_board.exs
│ │ ├── 20160111104041_add_description_to_cards.exs
│ │ ├── 20160111105636_create_comment.exs
│ │ ├── 20160113072454_user_password_fix.exs
│ │ ├── 20160115070933_add_slug_to_board.exs
│ │ ├── 20160120080228_create_card_member.exs
│ │ └── 20160127155137_add_tags_to_cards.exs
│ └── seeds.exs
└── static
│ ├── images
│ ├── favicon.png
│ └── logo.png
│ └── robots.txt
├── test
├── integration
│ ├── add_cards_test.exs
│ ├── add_lists_test.exs
│ ├── new_board_test.exs
│ ├── show_board_test.exs
│ ├── sign_in_test.exs
│ └── sign_up_test.exs
├── lib
│ └── phoenix_trello
│ │ └── board_channel
│ │ └── monitor_test.exs
├── models
│ ├── board_test.exs
│ ├── card_member_test.exs
│ ├── card_test.exs
│ ├── comment_test.exs
│ ├── list_test.exs
│ ├── user_board_test.exs
│ └── user_test.exs
├── support
│ ├── channel_case.ex
│ ├── conn_case.ex
│ ├── factory.ex
│ ├── integration_case.ex
│ └── model_case.ex
├── test_helper.exs
└── views
│ ├── error_view_test.exs
│ ├── layout_view_test.exs
│ └── page_view_test.exs
├── web
├── channels
│ ├── board_channel.ex
│ ├── user_channel.ex
│ └── user_socket.ex
├── controllers
│ ├── api
│ │ └── v1
│ │ │ ├── board_controller.ex
│ │ │ ├── card_controller.ex
│ │ │ ├── current_user_controller.ex
│ │ │ ├── registration_controller.ex
│ │ │ └── session_controller.ex
│ └── page_controller.ex
├── helpers
│ └── session.ex
├── models
│ ├── board.ex
│ ├── card.ex
│ ├── card_member.ex
│ ├── comment.ex
│ ├── list.ex
│ ├── user.ex
│ └── user_board.ex
├── router.ex
├── static
│ ├── css
│ │ ├── application.sass
│ │ ├── global
│ │ │ ├── _base.sass
│ │ │ ├── _grid_settings.sass
│ │ │ ├── _layout.sass
│ │ │ ├── _settings.sass
│ │ │ ├── _skin.sass
│ │ │ ├── _typography.sass
│ │ │ └── _utilities.sass
│ │ ├── libs
│ │ │ ├── _library-variable-overrides.sass
│ │ │ └── _normalize.sass
│ │ └── modules
│ │ │ ├── _boards.sass
│ │ │ ├── _example-module.sass
│ │ │ ├── _forms.sass
│ │ │ ├── _main_header.sass
│ │ │ ├── _members_selector.sass
│ │ │ ├── _modals.sass
│ │ │ ├── _modules.sass
│ │ │ └── _sessions.sass
│ └── js
│ │ ├── actions
│ │ ├── boards.js
│ │ ├── current_board.js
│ │ ├── current_card.js
│ │ ├── header.js
│ │ ├── lists.js
│ │ ├── registrations.js
│ │ └── sessions.js
│ │ ├── application.js
│ │ ├── components
│ │ ├── boards
│ │ │ ├── card.js
│ │ │ ├── form.js
│ │ │ └── members.js
│ │ ├── cards
│ │ │ ├── card.js
│ │ │ ├── form.js
│ │ │ ├── members_selector.js
│ │ │ ├── modal.js
│ │ │ └── tags_selector.js
│ │ └── lists
│ │ │ ├── card.js
│ │ │ └── form.js
│ │ ├── constants
│ │ ├── index.js
│ │ └── item_types.js
│ │ ├── containers
│ │ ├── authenticated.js
│ │ └── root.js
│ │ ├── layouts
│ │ ├── header.js
│ │ └── main.js
│ │ ├── reducers
│ │ ├── boards.js
│ │ ├── current_board.js
│ │ ├── current_card.js
│ │ ├── header.js
│ │ ├── index.js
│ │ ├── registration.js
│ │ └── session.js
│ │ ├── routes
│ │ └── index.js
│ │ ├── store
│ │ └── index.js
│ │ ├── utils
│ │ └── index.js
│ │ └── views
│ │ ├── boards
│ │ └── show.js
│ │ ├── cards
│ │ └── show.js
│ │ ├── home
│ │ └── index.js
│ │ ├── registrations
│ │ └── new.js
│ │ └── sessions
│ │ └── new.js
├── templates
│ ├── layout
│ │ └── app.html.eex
│ └── page
│ │ └── index.html.eex
├── views
│ ├── board_view.ex
│ ├── card_view.ex
│ ├── current_user_view.ex
│ ├── error_helpers.ex
│ ├── error_view.ex
│ ├── layout_view.ex
│ ├── page_view.ex
│ ├── registration_view.ex
│ └── session_view.ex
└── web.ex
└── webpack.config.js
/.buildpacks:
--------------------------------------------------------------------------------
1 | https://github.com/HashNuke/heroku-buildpack-elixir
2 | https://github.com/gjaldon/phoenix-static-buildpack
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # App artifacts
2 | /_build
3 | /db
4 | /deps
5 | /*.ez
6 |
7 | # Generate on crash by the VM
8 | erl_crash.dump
9 |
10 | # The config/prod.secret.exs file by default contains sensitive
11 | # data and you should not commit it into version control.
12 | #
13 | # Alternatively, you may comment the line below and commit the
14 | # secrets file as long as you replace its contents by environment
15 | # variables.
16 | /node_modules
17 | /config/dev.secret.exs
18 | /config/prod.secret.exs
19 | /priv/static/css/application.css
20 | /priv/static/js/application.js
21 |
22 | *.log
23 | priv/static
24 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: elixir
2 | elixir:
3 | - 1.2.1
4 | otp_release:
5 | - 18.1
6 | addons:
7 | postgresql: '9.4'
8 | services:
9 | - postgresql
10 | before_script:
11 | - cp config/travis.exs config/test.exs
12 | - mix do ecto.create, ecto.migrate
13 | script:
14 | - mix test --exclude integration
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Ricardo García Vega
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Phoenix Trello
2 | [](https://travis-ci.org/bigardone/phoenix-trello)
3 |
4 |
5 | [Trello](http://trello.com) tribute done with [Elixir](https://github.com/elixir-lang/elixir), [Phoenix Framework](https://github.com/phoenixframework/phoenix), [Webpack](https://github.com/webpack/webpack), [React](https://github.com/facebook/react) and [Redux](https://github.com/rackt/redux).
6 |
7 | 
8 |
9 | ## Tutorial
10 | 1. [Intro and selected stack](http://codeloveandboards.com/blog/2016/01/04/trello-tribute-with-phoenix-and-react-pt-1/)
11 | 2. [Phoenix Framework project setup](http://codeloveandboards.com/blog/2016/01/11/trello-tribute-with-phoenix-and-react-pt-2/)
12 | 3. [The User model and JWT auth](http://codeloveandboards.com/blog/2016/01/12/trello-tribute-with-phoenix-and-react-pt-3/)
13 | 4. [Front-end for sign up with React and Redux](http://codeloveandboards.com/blog/2016/01/14/trello-tribute-with-phoenix-and-react-pt-4/)
14 | 5. [Database seeding and sign in controller](http://codeloveandboards.com/blog/2016/01/18/trello-tribute-with-phoenix-and-react-pt-5/)
15 | 6. [Front-end authentication with React and Redux](http://codeloveandboards.com/blog/2016/01/20/trello-tribute-with-phoenix-and-react-pt-6/)
16 | 7. [Setting up sockets and channels](http://codeloveandboards.com/blog/2016/01/25/trello-tribute-with-phoenix-and-react-pt-7/)
17 | 8. [Listing and creating new boards](http://codeloveandboards.com/blog/2016/01/28/trello-tribute-with-phoenix-and-react-pt-8/)
18 | 9. [Adding board members](http://codeloveandboards.com/blog/2016/02/04/trello-tribute-with-phoenix-and-react-pt-9/)
19 | 10. [Tracking connected board members](http://codeloveandboards.com/blog/2016/02/15/trello-tribute-with-phoenix-and-react-pt-10/)
20 | 11. [Adding lists and cards](http://codeloveandboards.com/blog/2016/02/24/trello-tribute-with-phoenix-and-react-pt-11/)
21 | 12. [Deploying our application on Heroku](http://codeloveandboards.com/blog/2016/03/04/trello-tribute-with-phoenix-and-react-pt-12/)
22 |
23 | ## Live demo
24 | https://phoenix-trello.herokuapp.com
25 |
26 | ## Requirements
27 | You need to have **Elixir v1.3** and **PostgreSQL** installed.
28 |
29 | ## Installation instructions
30 | To start your Phoenix Trello app:
31 |
32 | 1. Install dependencies with `mix deps.get`
33 | 2. Ensure webpack is installed. ie: `npm install -g webpack`
34 | 3. Install npm packages with `npm install`
35 | 4. Create and migrate your database with `mix ecto.create && mix ecto.migrate`
36 | 5. Run seeds to create demo user with `mix run priv/repo/seeds.exs`
37 | 6. Start Phoenix endpoint with `mix phoenix.server`
38 |
39 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
40 |
41 | Enjoy!
42 |
43 | ## Testing
44 | Integration tests with [Hound](https://github.com/HashNuke/hound) and [Selenium ChromeDriver](https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver). Instructions:
45 |
46 | 1. Install **ChromeDriver** with `npm install -g chromedriver`
47 | 2. Run **ChromeDriver** in a new terminal window with `chromedriver &`
48 | 3. Run tests with `mix test`
49 |
50 | If you don't want to run integration tests just run `mix test --exclude integration`.
51 |
52 | ## License
53 |
54 | See [LICENSE](LICENSE).
55 |
--------------------------------------------------------------------------------
/compile:
--------------------------------------------------------------------------------
1 | info "****** Building Phoenix static assets"
2 | webpack
3 | mix phoenix.digest
4 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 | use Mix.Config
7 |
8 | config :phoenix_trello,
9 | namespace: PhoenixTrello,
10 | ecto_repos: [PhoenixTrello.Repo]
11 |
12 | # Configures the endpoint
13 | config :phoenix_trello, PhoenixTrello.Endpoint,
14 | url: [host: "localhost"],
15 | root: Path.dirname(__DIR__),
16 | secret_key_base: "hWbd3QwLuaWKwJY5qYOKLGSBboxjnW46c4TzBAa+cMODz26RokgHQIJo6Nej3DGr",
17 | render_errors: [accepts: ~w(html json)],
18 | pubsub: [name: PhoenixTrello.PubSub,
19 | adapter: Phoenix.PubSub.PG2]
20 |
21 | # Configures Elixir's Logger
22 | config :logger, :console,
23 | format: "$time $metadata[$level] $message\n",
24 | metadata: [:request_id]
25 |
26 | # Import environment specific config. This must remain at the bottom
27 | # of this file so it overrides the configuration defined above.
28 | import_config "#{Mix.env}.exs"
29 |
30 | # Configure phoenix generators
31 | config :phoenix, :generators,
32 | migration: true,
33 | binary_id: false
34 |
35 | # Configure guardian
36 | config :guardian, Guardian,
37 | issuer: "PhoenixTrello",
38 | ttl: { 3, :days },
39 | verify_issuer: true,
40 | serializer: PhoenixTrello.GuardianSerializer
41 |
42 | # Start Hound for PhantomJs
43 | config :hound, driver: "chrome_driver"
44 |
--------------------------------------------------------------------------------
/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 :phoenix_trello, PhoenixTrello.Endpoint,
10 | http: [port: 4000],
11 | debug_errors: true,
12 | code_reloader: true,
13 | cache_static_lookup: false,
14 | check_origin: false,
15 | watchers: [
16 | node: ["node_modules/webpack/bin/webpack.js", "--watch", "--color", cd: Path.expand("../", __DIR__)]
17 | ]
18 |
19 | # Watch static and templates for browser reloading.
20 | config :phoenix_trello, PhoenixTrello.Endpoint,
21 | live_reload: [
22 | patterns: [
23 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
24 | ~r{web/views/.*(ex)$},
25 | ~r{web/templates/.*(eex)$}
26 | ]
27 | ]
28 |
29 | # Do not include metadata nor timestamps in development logs
30 | config :logger, :console, format: "[$level] $message\n"
31 |
32 | # Set a higher stacktrace during development.
33 | # Do not configure such in production as keeping
34 | # and calculating stacktraces is usually expensive.
35 | config :phoenix, :stacktrace_depth, 20
36 |
37 | # Configure your database
38 | config :phoenix_trello, PhoenixTrello.Repo,
39 | adapter: Ecto.Adapters.Postgres,
40 | username: "postgres",
41 | password: "postgres",
42 | database: "phoenix_trello_dev",
43 | hostname: "localhost",
44 | pool_size: 10
45 |
46 | # Guardian configuration
47 | config :guardian, Guardian,
48 | secret_key: "W9cDv9fjPtsYv2gItOcFb5PzmRzqGkrOsJGmby0KpBOlHJIlhxMKFmIlcCG9PVFQ"
49 |
--------------------------------------------------------------------------------
/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, we configure the host to read the PORT
4 | # from the system environment. Therefore, you will need
5 | # to set PORT=80 before running your server.
6 | #
7 | # You should also configure the url host to something
8 | # meaningful, we use this information when generating URLs.
9 | #
10 | # Finally, we also include the path to a manifest
11 | # containing the digested version of static files. This
12 | # manifest is generated by the mix phoenix.digest task
13 | # which you typically run after static files are built.
14 | config :phoenix_trello, PhoenixTrello.Endpoint,
15 | http: [port: {:system, "PORT"}],
16 | url: [scheme: "https", host: "phoenix-trello.herokuapp.com", port: 443],
17 | force_ssl: [rewrite_on: [:x_forwarded_proto]],
18 | cache_static_manifest: "priv/static/manifest.json",
19 | secret_key_base: System.get_env("SECRET_KEY_BASE")
20 |
21 | # Do not print debug messages in production
22 | config :logger, level: :info
23 |
24 | # Configure your database
25 | config :phoenix_trello, PhoenixTrello.Repo,
26 | adapter: Ecto.Adapters.Postgres,
27 | url: System.get_env("DATABASE_URL"),
28 | pool_size: 20
29 |
30 | # Configure guardian
31 | config :guardian, Guardian,
32 | secret_key: System.get_env("GUARDIAN_SECRET_KEY")
33 |
34 | # ## SSL Support
35 | #
36 | # To get SSL working, you will need to add the `https` key
37 | # to the previous section and set your `:url` port to 443:
38 | #
39 | # config :phoenix_trello, PhoenixTrello.Endpoint,
40 | # ...
41 | # url: [host: "example.com", port: 443],
42 | # https: [port: 443,
43 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
44 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
45 | #
46 | # Where those two env variables return an absolute path to
47 | # the key and cert in disk or a relative path inside priv,
48 | # for example "priv/ssl/server.key".
49 | #
50 | # We also recommend setting `force_ssl`, ensuring no data is
51 | # ever sent via http, always redirecting to https:
52 | #
53 | # config :phoenix_trello, PhoenixTrello.Endpoint,
54 | # force_ssl: [hsts: true]
55 | #
56 | # Check `Plug.SSL` for all available options in `force_ssl`.
57 |
58 | # ## Using releases
59 | #
60 | # If you are doing OTP releases, you need to instruct Phoenix
61 | # to start the server for all endpoints:
62 | #
63 | # config :phoenix, :serve_endpoints, true
64 | #
65 | # Alternatively, you can configure exactly which server to
66 | # start per endpoint:
67 | #
68 | # config :phoenix_trello, PhoenixTrello.Endpoint, server: true
69 | #
70 |
71 | # Finally import the config/prod.secret.exs
72 | # which should be versioned separately.
73 | # import_config "prod.secret.exs"
74 |
--------------------------------------------------------------------------------
/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 :phoenix_trello, PhoenixTrello.Endpoint,
6 | http: [port: 4001],
7 | server: true
8 |
9 | # Print only warnings and errors during test
10 | config :logger, level: :warn
11 |
12 | # Configure your database
13 | config :phoenix_trello, PhoenixTrello.Repo,
14 | adapter: Ecto.Adapters.Postgres,
15 | username: "postgres",
16 | password: "postgres",
17 | database: "phoenix_trello_test",
18 | hostname: "localhost",
19 | pool: Ecto.Adapters.SQL.Sandbox
20 |
21 | # Guardian configuration
22 | config :guardian, Guardian,
23 | secret_key: "W9cDv9fjPtsYv2gItOcFb5PzmRzqGkrOsJGmby0KpBOlHJIlhxMKFmIlcCG9PVFQ"
24 |
--------------------------------------------------------------------------------
/config/travis.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 :phoenix_trello, PhoenixTrello.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 :phoenix_trello, PhoenixTrello.Repo,
14 | adapter: Ecto.Adapters.Postgres,
15 | username: "postgres",
16 | password: "",
17 | database: "phoenix_trello_test",
18 | hostname: "localhost",
19 | pool: Ecto.Adapters.SQL.Sandbox
20 |
--------------------------------------------------------------------------------
/elixir_buildpack.config:
--------------------------------------------------------------------------------
1 | # Elixir version
2 | elixir_version=1.2.1
3 |
4 | # Always rebuild from scratch on every deploy?
5 | always_rebuild=true
6 |
--------------------------------------------------------------------------------
/lib/phoenix_trello.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello do
2 | use Application
3 |
4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html
5 | # for more information on OTP Applications
6 | def start(_type, _args) do
7 | import Supervisor.Spec, warn: false
8 |
9 | children = [
10 | # Start the endpoint when the application starts
11 | supervisor(PhoenixTrello.Endpoint, []),
12 | # Start the Ecto repository
13 | worker(PhoenixTrello.Repo, []),
14 | # Here you could define other workers and supervisors as children
15 | # worker(PhoenixTrello.Worker, [arg1, arg2, arg3]),
16 | supervisor(PhoenixTrello.BoardChannel.Supervisor, []),
17 | ]
18 |
19 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
20 | # for other strategies and supported options
21 | opts = [strategy: :one_for_one, name: PhoenixTrello.Supervisor]
22 | Supervisor.start_link(children, opts)
23 | end
24 |
25 | # Tell Phoenix to update the endpoint configuration
26 | # whenever the application is updated.
27 | def config_change(changed, _new, removed) do
28 | PhoenixTrello.Endpoint.config_change(changed, removed)
29 | :ok
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/phoenix_trello/board_channel/monitor.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.BoardChannel.Monitor do
2 | @moduledoc """
3 | Board monitor that keeps track of connected users.
4 | """
5 |
6 | use GenServer
7 |
8 | #####
9 | # External API
10 |
11 | def create(board_id) do
12 | case GenServer.whereis(ref(board_id)) do
13 | nil ->
14 | Supervisor.start_child(PhoenixTrello.BoardChannel.Supervisor, [board_id])
15 | _board ->
16 | {:error, :board_already_exists}
17 | end
18 | end
19 |
20 | def start_link(board_id) do
21 | GenServer.start_link(__MODULE__, [], name: ref(board_id))
22 | end
23 |
24 | def user_joined(board_id, user) do
25 | try_call board_id, {:user_joined, user}
26 | end
27 |
28 | def users_in_board(board_id) do
29 | try_call board_id, {:users_in_board}
30 | end
31 |
32 | def user_left(board_id, user) do
33 | try_call board_id, {:user_left, user}
34 | end
35 |
36 | #####
37 | # GenServer implementation
38 |
39 | def handle_call({:user_joined, user}, _from, users) do
40 | users = [user] ++ users
41 | |> Enum.uniq
42 |
43 | {:reply, users, users}
44 | end
45 |
46 | def handle_call({:users_in_board}, _from, users) do
47 | { :reply, users, users }
48 | end
49 |
50 | def handle_call({:user_left, user}, _from, users) do
51 | users = List.delete(users, user)
52 | {:reply, users, users}
53 | end
54 |
55 | defp ref(board_id) do
56 | {:global, {:board, board_id}}
57 | end
58 |
59 | defp try_call(board_id, call_function) do
60 | case GenServer.whereis(ref(board_id)) do
61 | nil ->
62 | {:error, :invalid_board}
63 | board ->
64 | GenServer.call(board, call_function)
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/phoenix_trello/board_channel/supervisor.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.BoardChannel.Supervisor do
2 | use Supervisor
3 |
4 | def start_link do
5 | Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
6 | end
7 |
8 | def init(:ok) do
9 | children = [
10 | worker(PhoenixTrello.BoardChannel.Monitor, [], restart: :temporary)
11 | ]
12 |
13 | supervise(children, strategy: :simple_one_for_one)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/phoenix_trello/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :phoenix_trello
3 |
4 | socket "/socket", PhoenixTrello.UserSocket
5 |
6 | # Serve at "/" the static files from "priv/static" directory.
7 | #
8 | # You should set gzip to true if you are running phoenix.digest
9 | # when deploying your static files in production.
10 | plug Plug.Static,
11 | at: "/", from: :phoenix_trello, gzip: false,
12 | only: ~w(css fonts images js favicon.ico robots.txt)
13 |
14 | # Code reloading can be explicitly enabled under the
15 | # :code_reloader configuration of your endpoint.
16 | if code_reloading? do
17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
18 | plug Phoenix.LiveReloader
19 | plug Phoenix.CodeReloader
20 | end
21 |
22 | plug Plug.RequestId
23 | plug Plug.Logger
24 |
25 | plug Plug.Parsers,
26 | parsers: [:urlencoded, :multipart, :json],
27 | pass: ["*/*"],
28 | json_decoder: Poison
29 |
30 | plug Plug.MethodOverride
31 | plug Plug.Head
32 |
33 | plug Plug.Session,
34 | store: :cookie,
35 | key: "_phoenix_trello_key",
36 | signing_salt: "utLqshqa"
37 |
38 | plug PhoenixTrello.Router
39 | end
40 |
--------------------------------------------------------------------------------
/lib/phoenix_trello/guardian_serializer.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.GuardianSerializer do
2 | @behaviour Guardian.Serializer
3 |
4 | alias PhoenixTrello.{Repo, User}
5 |
6 | def for_token(user = %User{}), do: { :ok, "User:#{user.id}" }
7 | def for_token(_), do: { :error, "Unknown resource type" }
8 |
9 | def from_token("User:" <> id), do: { :ok, Repo.get(User, String.to_integer(id)) }
10 | def from_token(_), do: { :error, "Unknown resource type" }
11 | end
12 |
--------------------------------------------------------------------------------
/lib/phoenix_trello/permalink.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Permalink do
2 | @behaviour Ecto.Type
3 |
4 | def type, do: :id
5 |
6 | def cast(binary) when is_binary(binary) do
7 | case Integer.parse(binary) do
8 | {int, _} when int > 0 -> {:ok, int}
9 | _ -> :error
10 | end
11 | end
12 |
13 | def cast(integer) when is_integer(integer) do
14 | {:ok, integer}
15 | end
16 |
17 | def cast(_) do
18 | :error
19 | end
20 |
21 | def dump(integer) when is_integer(integer) do
22 | {:ok, integer}
23 | end
24 |
25 | def load(integer) when is_integer(integer) do
26 | {:ok, integer}
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/phoenix_trello/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Repo do
2 | use Ecto.Repo, otp_app: :phoenix_trello
3 | end
4 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :phoenix_trello,
6 | version: "0.0.1",
7 | elixir: "~> 1.0",
8 | elixirc_paths: elixirc_paths(Mix.env),
9 | compilers: [:phoenix] ++ Mix.compilers,
10 | build_embedded: Mix.env == :prod,
11 | start_permanent: Mix.env == :prod,
12 | aliases: aliases,
13 | deps: deps]
14 | end
15 |
16 | # Configuration for the OTP application.
17 | #
18 | # Type `mix help compile.app` for more information.
19 | def application do
20 | [mod: {PhoenixTrello, []},
21 | applications: [
22 | :phoenix,
23 | :phoenix_pubsub,
24 | :phoenix_html,
25 | :cowboy,
26 | :logger,
27 | :phoenix_ecto,
28 | :postgrex,
29 | :comeonin,
30 | :ex_machina
31 | ]]
32 | end
33 |
34 | # Specifies which paths to compile per environment.
35 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
36 | defp elixirc_paths(_), do: ["lib", "web"]
37 |
38 | # Specifies your project dependencies.
39 | #
40 | # Type `mix help deps` for examples and options.
41 | defp deps do
42 | [
43 | {:phoenix, "~> 1.2.1"},
44 | {:phoenix_pubsub, "~> 1.0"},
45 | {:phoenix_ecto, "~> 3.0.1"},
46 | {:postgrex, ">= 0.0.0", override: true},
47 | {:phoenix_html, "~> 2.6.2"},
48 | {:phoenix_live_reload, "~> 1.0", only: :dev},
49 | {:cowboy, "~> 1.0"},
50 | {:comeonin, "~> 2.5.3"},
51 | {:guardian, "~> 0.13.0"},
52 | {:credo, "~> 0.4.11", only: [:dev, :test]},
53 | {:ex_machina, "~> 1.0.2"},
54 | {:exactor, "~> 2.2.0"},
55 | {:hound, "~> 1.0.2"},
56 | {:mix_test_watch, "~> 0.2", only: :dev}
57 | ]
58 | end
59 |
60 | # Aliases are shortcut or tasks specific to the current project.
61 | # For example, to create, migrate and run the seeds file at once:
62 | #
63 | # $ mix ecto.setup
64 | #
65 | # See the documentation for `Mix` for more info on aliases.
66 | defp aliases do
67 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
68 | "ecto.reset": ["ecto.drop", "ecto.setup"],
69 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]]
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{"backoff": {:hex, :backoff, "1.1.1"},
2 | "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], []},
3 | "bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []},
4 | "certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []},
5 | "comeonin": {:hex, :comeonin, "2.5.3", "ccd70ebf465eaf4e11fc5a13fd0f5be2538b6b4975d4f7a13d571670b31da060", [:mix, :make, :make], []},
6 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []},
7 | "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:rebar, :make], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]},
8 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []},
9 | "credo": {:hex, :credo, "0.4.11", "03a64e9d53309b7132556284dda0be57ba1013885725124cfea7748d740c6170", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]},
10 | "db_connection": {:hex, :db_connection, "1.0.0-rc.5", "1d9ab6e01387bdf2de7a16c56866971f7c2f75aea7c69cae2a0346e4b537ae0d", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0.0-beta.3", [hex: :sbroker, optional: true]}]},
11 | "decimal": {:hex, :decimal, "1.1.2", "79a769d4657b2d537b51ef3c02d29ab7141d2b486b516c109642d453ee08e00c", [:mix], []},
12 | "ecto": {:hex, :ecto, "2.0.5", "7f4c79ac41ffba1a4c032b69d7045489f0069c256de606523c65d9f8188e502d", [:mix], [{:db_connection, "~> 1.0-rc.4", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.1.2 or ~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.7.7", [hex: :mariaex, optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.12.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, optional: true]}]},
13 | "ex_machina": {:hex, :ex_machina, "1.0.2", "1cc49e1a09e3f7ab2ecb630c17f14c2872dc4ec145d6d05a9c3621936a63e34f", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: true]}]},
14 | "exactor": {:hex, :exactor, "2.2.2", "90b27d72c05614801a60f8400afd4e4346dfc33ea9beffe3b98a794891d2ff96", [:mix], []},
15 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []},
16 | "guardian": {:hex, :guardian, "0.13.0", "37c5b5302617276093570ee938baca146f53e1d5de1f5c2b8effb1d2fea596d2", [:mix], [{:jose, "~> 1.8", [hex: :jose, optional: false]}, {:phoenix, "~> 1.2.0", [hex: :phoenix, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}, {:poison, ">= 1.3.0", [hex: :poison, optional: false]}, {:uuid, ">=1.1.1", [hex: :uuid, optional: false]}]},
17 | "hackney": {:hex, :hackney, "1.6.1", "ddd22d42db2b50e6a155439c8811b8f6df61a4395de10509714ad2751c6da817", [:rebar3], [{:certifi, "0.4.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}]},
18 | "hound": {:hex, :hound, "1.0.2", "b6dd20142d00c28009fad503a23fa4a76bc11899e0d198f36a9c1448427788b2", [:mix], [{:httpoison, "~> 0.8", [hex: :httpoison, optional: false]}, {:poison, ">= 1.4.0", [hex: :poison, optional: false]}]},
19 | "httpoison": {:hex, :httpoison, "0.9.1", "6c2b4eaf2588a6f3ef29663d28c992531ca3f0bc832a97e0359bc822978e1c5d", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]},
20 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []},
21 | "jose": {:hex, :jose, "1.8.0", "1ee027c5c0ff3922e3bfe58f7891509e8f87f771ba609ee859e623cc60237574", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, optional: false]}]},
22 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
23 | "mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], []},
24 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []},
25 | "mix_test_watch": {:hex, :mix_test_watch, "0.2.6", "9fcc2b1b89d1594c4a8300959c19d50da2f0ff13642c8f681692a6e507f92cab", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}]},
26 | "phoenix": {:hex, :phoenix, "1.2.1", "6dc592249ab73c67575769765b66ad164ad25d83defa3492dc6ae269bd2a68ab", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.1", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]},
27 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.0.1", "42eb486ef732cf209d0a353e791806721f33ff40beab0a86f02070a5649ed00a", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.6", [hex: :phoenix_html, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]},
28 | "phoenix_html": {:hex, :phoenix_html, "2.6.2", "944a5e581b0d899e4f4c838a69503ebd05300fe35ba228a74439e6253e10e0c0", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]},
29 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.5", "829218c4152ba1e9848e2bf8e161fcde6b4ec679a516259442561d21fde68d0b", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, optional: false]}]},
30 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.0", "c31af4be22afeeebfaf246592778c8c840e5a1ddc7ca87610c41ccfb160c2c57", [:mix], []},
31 | "plug": {:hex, :plug, "1.2.0", "496bef96634a49d7803ab2671482f0c5ce9ce0b7b9bc25bc0ae8e09859dd2004", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]},
32 | "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []},
33 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []},
34 | "postgrex": {:hex, :postgrex, "0.12.0", "bdeeb4c42768c47c3c92228e66c70357fe9a9384fbc9de06abba774b22dd0635", [:mix], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.0-rc.4", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]},
35 | "ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []},
36 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []},
37 | "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"},
38 | "uuid": {:hex, :uuid, "1.1.5", "96cb36d86ee82f912efea4d50464a5df606bf3f1163d6bdbb302d98474969369", [:mix], []}}
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "phoenix_trello",
3 | "version": "1.0.0",
4 | "description": "Trello tribute done in Elixir and Phoenix.",
5 | "main": "index.js",
6 | "directories": {
7 | "test": "test"
8 | },
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "author": "Ricardo García Vega",
13 | "license": "ISC",
14 | "private": true,
15 | "engines": {
16 | "node": "5.3.0",
17 | "npm": "3.5.x"
18 | },
19 | "devDependencies": {
20 | "babel-core": "^6.3.26",
21 | "babel-loader": "^6.2.0",
22 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
23 | "babel-preset-es2015": "^6.6.0",
24 | "babel-preset-react": "^6.3.13",
25 | "babel-preset-stage-0": "^6.3.13",
26 | "babel-preset-stage-2": "^6.3.13",
27 | "css-loader": "^0.23.1",
28 | "extract-text-webpack-plugin": "^1.0.1",
29 | "node-libs-browser": "^1.0.0",
30 | "node-sass": "^3.4.2",
31 | "sass-loader": "^3.1.2",
32 | "style-loader": "^0.13.0",
33 | "webpack": "^1.12.11"
34 | },
35 | "dependencies": {
36 | "bourbon": "^4.2.6",
37 | "bourbon-neat": "^1.7.2",
38 | "classnames": "^2.2.1",
39 | "es6-promise": "^3.0.2",
40 | "history": "^1.17.0",
41 | "invariant": "^2.2.0",
42 | "isomorphic-fetch": "^2.2.1",
43 | "moment": "^2.11.1",
44 | "phoenix": "file:deps/phoenix",
45 | "phoenix_html": "file:deps/phoenix_html",
46 | "react": "^0.14.5",
47 | "react-addons-css-transition-group": "^0.14.5",
48 | "react-dnd": "^2.0.2",
49 | "react-dnd-html5-backend": "^2.0.2",
50 | "react-dom": "^0.14.5",
51 | "react-gravatar": "^2.2.2",
52 | "react-page-click": "^2.0.0",
53 | "react-redux": "^4.0.4",
54 | "react-router": "^2.0.1",
55 | "react-router-redux": "^4.0.0",
56 | "redux": "^3.3.1",
57 | "redux-logger": "^2.3.2",
58 | "redux-thunk": "^1.0.2"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/phoenix_static_buildpack.config:
--------------------------------------------------------------------------------
1 | # We can set the version of Node to use for the app here
2 | node_version=5.3.0
3 |
4 | # We can set the version of NPM to use for the app here
5 | npm_version=3.5.2
6 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20151224075404_create_user.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Repo.Migrations.CreateUser do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:users) do
6 | add :first_name, :string, null: false
7 | add :last_name, :string, null: false
8 | add :email, :string, null: false
9 | add :crypted_password, :string, null: false
10 |
11 | timestamps
12 | end
13 |
14 | create unique_index(:users, [:email])
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20151224093233_create_board.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Repo.Migrations.CreateBoard do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:boards) do
6 | add :name, :string, null: false
7 |
8 | add :user_id, references(:users, on_delete: :delete_all), null: false
9 |
10 | timestamps
11 | end
12 |
13 | create index(:boards, [:user_id])
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20151225091657_create_list.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Repo.Migrations.CreateList do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:lists) do
6 | add :name, :string, null: false
7 | add :position, :integer, defaul: 0
8 | add :board_id, references(:boards, on_delete: :delete_all)
9 |
10 | timestamps
11 | end
12 |
13 | create index(:lists, [:board_id])
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20151227084131_create_card.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Repo.Migrations.CreateCard do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:cards) do
6 | add :name, :string, null: false
7 | add :position, :integer, default: 0
8 | add :list_id, references(:lists, on_delete: :delete_all), null: false
9 |
10 | timestamps
11 | end
12 |
13 | create index(:cards, [:list_id])
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20151230081546_create_user_board.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Repo.Migrations.CreateUserBoard do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:user_boards) do
6 | add :user_id, references(:users, on_delete: :delete_all), null: false
7 | add :board_id, references(:boards, on_delete: :delete_all), null: false
8 |
9 | timestamps
10 | end
11 |
12 | create index(:user_boards, [:user_id])
13 | create index(:user_boards, [:board_id])
14 | create unique_index(:user_boards, [:user_id, :board_id])
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20160111104041_add_description_to_cards.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Repo.Migrations.AddDescriptionToCards do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:cards) do
6 | add :description, :text
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20160111105636_create_comment.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Repo.Migrations.CreateComment do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:comments) do
6 | add :text, :string, null: false
7 | add :user_id, references(:users, on_delete: :delete_all)
8 | add :card_id, references(:cards, on_delete: :delete_all)
9 |
10 | timestamps
11 | end
12 |
13 | create index(:comments, [:user_id])
14 | create index(:comments, [:card_id])
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20160113072454_user_password_fix.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Repo.Migrations.UserPasswordFix do
2 | use Ecto.Migration
3 |
4 | def change do
5 | rename table(:users), :crypted_password, to: :encrypted_password
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20160115070933_add_slug_to_board.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Repo.Migrations.AddSlugToBoard do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:boards) do
6 | add :slug, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20160120080228_create_card_member.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Repo.Migrations.CreateCardMember do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:card_members) do
6 | add :card_id, references(:cards, on_delete: :delete_all), null: false
7 | add :user_board_id, references(:user_boards, on_delete: :delete_all), null: false
8 |
9 | timestamps
10 | end
11 |
12 | create index(:card_members, [:card_id])
13 | create index(:card_members, [:user_board_id])
14 | create unique_index(:card_members, [:card_id, :user_board_id])
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20160127155137_add_tags_to_cards.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Repo.Migrations.AddTagsToCards do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:cards) do
6 | add :tags, {:array, :string}, default: []
7 | end
8 |
9 | create index(:cards, [:tags])
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | alias PhoenixTrello.{Repo, User}
2 |
3 | [
4 | %{
5 | first_name: "John",
6 | last_name: "Doe",
7 | email: "john@phoenix-trello.com",
8 | password: "12345678"
9 | },
10 | ]
11 | |> Enum.map(&User.changeset(%User{}, &1))
12 | |> Enum.each(&Repo.insert!(&1))
13 |
--------------------------------------------------------------------------------
/priv/static/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bigardone/phoenix-trello/60c874de63126b5659a42ef8e05846ca16e3e30a/priv/static/images/favicon.png
--------------------------------------------------------------------------------
/priv/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bigardone/phoenix-trello/60c874de63126b5659a42ef8e05846ca16e3e30a/priv/static/images/logo.png
--------------------------------------------------------------------------------
/priv/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/test/integration/add_cards_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.AddCardsTest do
2 | use PhoenixTrello.IntegrationCase
3 |
4 | alias PhoenixTrello.{Board}
5 |
6 | setup do
7 | user = create_user
8 | board = create_board(user)
9 |
10 | list = board
11 | |> build_assoc(:lists)
12 | |> PhoenixTrello.List.changeset(%{name: "First list"})
13 | |> Repo.insert!
14 |
15 |
16 | {:ok, %{user: user, board: board, list: list}}
17 | end
18 |
19 | @tag :integration
20 | test "Clicking on a previously created list", %{user: user, board: board, list: list} do
21 | user_sign_in(%{user: user, board: board})
22 |
23 | navigate_to "/boards/#{Board.slug_id(board)}"
24 |
25 | assert element_displayed?({:css, ".view-container.boards.show"})
26 |
27 | assert element_displayed?({:id, "list_#{list.id}"})
28 |
29 | find_element(:id, "list_#{list.id}")
30 | |> find_within_element(:css, ".add-new")
31 | |> click
32 |
33 | assert element_displayed?({:id, "new_card_form"})
34 |
35 | new_card_form = find_element(:id, "new_card_form")
36 |
37 | new_card_form
38 | |> find_within_element(:id, "card_name")
39 | |> fill_field("New card")
40 |
41 | new_card_form
42 | |> find_within_element(:css, "button")
43 | |> click
44 |
45 | card = board
46 | |> last_card
47 |
48 | assert element_displayed?({:id, "card_#{card.id}"})
49 | assert page_source =~ card.name
50 | end
51 |
52 | defp last_card(board) do
53 | Board
54 | |> Repo.get!(board.id)
55 | |> Repo.preload([:cards])
56 | |> Map.get(:cards)
57 | |> List.last
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/test/integration/add_lists_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.AddListsTest do
2 | use PhoenixTrello.IntegrationCase
3 |
4 | alias PhoenixTrello.{Board}
5 |
6 | setup do
7 | user = create_user
8 | board = create_board(user)
9 |
10 | {:ok, %{user: user, board: board }}
11 | end
12 |
13 | @tag :integration
14 | test "Clicking on previously created board", %{user: user, board: board} do
15 | user_sign_in(%{user: user, board: board})
16 |
17 | navigate_to "/boards/#{Board.slug_id(board)}"
18 |
19 | assert element_displayed?({:css, ".view-container.boards.show"})
20 | assert page_source =~ "Add new list..."
21 |
22 | click {:css, ".list.add-new"}
23 |
24 | assert element_displayed?({:css, ".list.form"})
25 |
26 | new_list_form = find_element(:id, "new_list_form")
27 |
28 | new_list_form
29 | |> find_within_element(:id, "list_name")
30 | |> fill_field("New list")
31 |
32 | new_list_form
33 | |> find_within_element(:css, "button")
34 | |> click
35 |
36 | list = user
37 | |> last_list
38 |
39 | assert element_displayed?({:id, "list_#{list.id}"})
40 | end
41 |
42 | defp last_list(user) do
43 | user
44 | |> Repo.preload([boards: [:lists]])
45 | |> Map.get(:boards)
46 | |> List.last
47 | |> Repo.preload(:lists)
48 | |> Map.get(:lists)
49 | |> List.last
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/test/integration/new_board_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.NewBoardTest do
2 | use PhoenixTrello.IntegrationCase
3 |
4 | setup do
5 | user = create_user
6 |
7 | {:ok, %{user: user}}
8 | end
9 |
10 | @tag :integration
11 | test "GET / with existing user", %{user: user} do
12 | user_sign_in(%{user: user})
13 |
14 | click({:id, "add_new_board"})
15 |
16 | assert element_displayed?({:id, "new_board_form"})
17 |
18 | new_board_form = find_element(:id, "new_board_form")
19 |
20 | new_board_form
21 | |> find_within_element(:id, "board_name")
22 | |> fill_field("New board")
23 |
24 | new_board_form
25 | |> find_within_element(:css, "button")
26 | |> click
27 |
28 | assert element_displayed?({:css, ".view-container.boards.show"})
29 |
30 | board = last_board(user)
31 |
32 | assert page_title =~ board.name
33 | assert page_source =~ "New board"
34 | assert page_source =~ "Add new list..."
35 | end
36 |
37 | def last_board(user) do
38 | user
39 | |> Repo.preload(:boards)
40 | |> Map.get(:boards)
41 | |> Enum.at(0)
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/test/integration/show_board_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.ShowBoardTest do
2 | use PhoenixTrello.IntegrationCase
3 |
4 | alias PhoenixTrello.{Board}
5 |
6 | setup do
7 | user = create_user
8 | board = create_board(user)
9 |
10 | {:ok, %{user: user, board: board |> Repo.preload([:user, :members, lists: :cards])}}
11 | end
12 |
13 | @tag :integration
14 | test "Clicking on previously created board", %{user: user, board: board} do
15 | user_sign_in(%{user: user, board: board})
16 |
17 | assert page_source =~ board.name
18 |
19 | board_id = board
20 | |> Board.slug_id
21 |
22 | click({:id, board_id})
23 |
24 | assert element_displayed?({:css, ".view-container.boards.show"})
25 |
26 | assert page_title =~ board.name
27 | assert page_source =~ board.name
28 | assert page_source =~ "Add new list..."
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/integration/sign_in_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.SignInTest do
2 | use PhoenixTrello.IntegrationCase
3 |
4 | @tag :integration
5 | test "GET /" do
6 | navigate_to "/"
7 |
8 | assert page_title == "Sign in | Phoenix Trello"
9 | assert element_displayed?({:id, "sign_in_form"})
10 | end
11 |
12 | @tag :integration
13 | test "Sign in with wrong email/password" do
14 | navigate_to "/"
15 |
16 | assert element_displayed?({:id, "sign_in_form"})
17 |
18 | sign_in_form = find_element(:id, "sign_in_form")
19 |
20 | sign_in_form
21 | |> find_within_element(:id, "user_email")
22 | |> fill_field("incorrect@email.com")
23 |
24 | sign_in_form
25 | |> find_within_element(:css, "button")
26 | |> click
27 |
28 | assert element_displayed?({:class, "error"})
29 |
30 | assert page_source =~ "Invalid email or password"
31 | end
32 |
33 | @tag :integration
34 | test "Sign in with existing email/password" do
35 | user = create_user
36 |
37 | user_sign_in(%{user: user})
38 |
39 | assert page_source =~ "#{user.first_name} #{user.last_name}"
40 | assert page_source =~ "My boards"
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/test/integration/sign_up_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.SignUpTest do
2 | use PhoenixTrello.IntegrationCase
3 |
4 | @tag :integration
5 | test "GET /sign_up" do
6 | navigate_to "/sign_up"
7 |
8 | assert page_title == "Sign up | Phoenix Trello"
9 | assert element_displayed?({:id, "sign_up_form"})
10 | end
11 |
12 | @tag :integration
13 | test "Siginig up with correct data" do
14 | navigate_to "/sign_up"
15 |
16 | assert element_displayed?({:id, "sign_up_form"})
17 |
18 | sign_up_form = find_element(:id, "sign_up_form")
19 |
20 | sign_up_form
21 | |> find_within_element(:id, "user_first_name")
22 | |> fill_field("John")
23 |
24 | sign_up_form
25 | |> find_within_element(:id, "user_last_name")
26 | |> fill_field("Doe")
27 |
28 | sign_up_form
29 | |> find_within_element(:id, "user_email")
30 | |> fill_field("john@doe.com")
31 |
32 | sign_up_form
33 | |> find_within_element(:id, "user_password")
34 | |> fill_field("12345678")
35 |
36 | sign_up_form
37 | |> find_within_element(:id, "user_password_confirmation")
38 | |> fill_field("12345678")
39 |
40 | sign_up_form
41 | |> find_within_element(:css, "button")
42 | |> click
43 |
44 | assert element_displayed?({:id, "authentication_container"})
45 |
46 | assert page_source =~ "John Doe"
47 | assert page_source =~ "My boards"
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/test/lib/phoenix_trello/board_channel/monitor_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.BoardChannel.MonitorTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias PhoenixTrello.{BoardChannel.Monitor, User}
5 |
6 | @board_id "1-board"
7 |
8 | setup_all do
9 | users = %{
10 | first_user: %User{id: 1},
11 | second_user: %User{id: 2},
12 | third_user: %User{id: 3}
13 | }
14 |
15 | Monitor.create(@board_id)
16 |
17 | {:ok, %{users: users}}
18 | end
19 |
20 | test "it adds a user calling :user_joined", %{users: users} do
21 | Monitor.user_joined(@board_id, users.first_user.id)
22 | Monitor.user_joined(@board_id, users.second_user.id)
23 | new_state = Monitor.user_joined(@board_id, users.third_user.id)
24 |
25 | assert new_state == [users.third_user.id, users.second_user.id, users.first_user.id]
26 | end
27 |
28 | test "it removes a user when calling :user_left", %{users: users} do
29 | Monitor.user_joined(@board_id, users.first_user.id)
30 | Monitor.user_joined(@board_id, users.second_user.id)
31 | new_state = Monitor.user_joined(@board_id, users.third_user.id)
32 | assert new_state == [users.third_user.id, users.second_user.id, users.first_user.id]
33 |
34 | new_state = Monitor.user_left(@board_id, users.third_user.id)
35 | assert new_state == [users.second_user.id, users.first_user.id]
36 |
37 | new_state = Monitor.user_left(@board_id, users.second_user.id)
38 | assert new_state == [users.first_user.id]
39 | end
40 |
41 | test "it returns the list of users in channel when calling :users_in_board", %{users: users} do
42 | Monitor.user_joined(@board_id, users.first_user.id)
43 | Monitor.user_joined(@board_id, users.second_user.id)
44 | Monitor.user_joined(@board_id, users.third_user.id)
45 |
46 | returned_users = Monitor.users_in_board(@board_id)
47 |
48 | assert returned_users === [users.third_user.id, users.second_user.id, users.first_user.id]
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/test/models/board_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.BoardTest do
2 | use PhoenixTrello.ModelCase
3 |
4 | import PhoenixTrello.Factory
5 |
6 | alias PhoenixTrello.Board
7 |
8 | @valid_attrs %{name: "some content"}
9 | @invalid_attrs %{}
10 |
11 | test "changeset with valid attributes" do
12 | user = insert(:user)
13 | attributes = @valid_attrs
14 | |> Map.put(:user_id, user.id)
15 |
16 | changeset = Board.changeset(build(:board), attributes)
17 | assert changeset.valid?
18 |
19 | %{slug: slug} = changeset.changes
20 | assert slug == "some-content"
21 | end
22 |
23 | test "changeset with invalid attributes" do
24 | changeset = Board.changeset(%Board{}, @invalid_attrs)
25 | refute changeset.valid?
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/test/models/card_member_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.CardMemberTest do
2 | use PhoenixTrello.ModelCase
3 |
4 | alias PhoenixTrello.CardMember
5 |
6 | @valid_attrs %{card_id: 1, user_board_id: 1}
7 | @invalid_attrs %{}
8 |
9 | test "changeset with valid attributes" do
10 | changeset = CardMember.changeset(%CardMember{}, @valid_attrs)
11 | assert changeset.valid?
12 | end
13 |
14 | test "changeset with invalid attributes" do
15 | changeset = CardMember.changeset(%CardMember{}, @invalid_attrs)
16 | refute changeset.valid?
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/models/card_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.CardTest do
2 | use PhoenixTrello.ModelCase
3 |
4 | import PhoenixTrello.Factory
5 |
6 | alias PhoenixTrello.{Card}
7 |
8 | @valid_attrs %{name: "some content"}
9 | @invalid_attrs %{}
10 |
11 | setup do
12 | user = insert(:user)
13 | board = insert(:board, %{user: user})
14 | list = insert(:list, %{board: board})
15 |
16 | {:ok, list: list}
17 | end
18 |
19 | test "existing cards for the same list", %{list: list} do
20 | count = 3
21 |
22 | for i <- 1..(count-1) do
23 | list
24 | |> build_assoc(:cards)
25 | |> Card.changeset(%{name: "Card #{i}"})
26 | |> Repo.insert
27 | end
28 |
29 | {:ok, last_card} = list
30 | |> build_assoc(:cards)
31 | |> Card.changeset(%{name: "Last"})
32 | |> Repo.insert
33 |
34 | assert last_card.position == 1024 * count
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/test/models/comment_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.CommentTest do
2 | use PhoenixTrello.ModelCase
3 |
4 | import PhoenixTrello.Factory
5 |
6 | alias PhoenixTrello.Comment
7 |
8 | @valid_attrs %{text: "some content"}
9 | @invalid_attrs %{}
10 |
11 | setup do
12 | list = insert(:list_with_cards)
13 | |> Repo.preload([:board, :cards])
14 |
15 | {:ok, list: list, card: List.first(list.cards)}
16 | end
17 |
18 | test "changeset with valid attributes", %{list: list, card: card} do
19 | attributes = @valid_attrs
20 | |> Map.put(:user_id, list.board.user_id)
21 | |> Map.put(:card_id, card.id)
22 |
23 | changeset = Comment.changeset(%Comment{}, attributes)
24 | assert changeset.valid?
25 | end
26 |
27 | test "changeset with invalid attributes" do
28 | changeset = Comment.changeset(%Comment{}, @invalid_attrs)
29 | refute changeset.valid?
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/test/models/list_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.ListTest do
2 | use PhoenixTrello.ModelCase
3 |
4 | import PhoenixTrello.Factory
5 |
6 | alias PhoenixTrello.{Repo, List}
7 |
8 | @valid_attrs %{name: "some content"}
9 | @invalid_attrs %{}
10 |
11 | setup do
12 | {:ok, board: insert(:board)}
13 | end
14 |
15 | test "changeset with valid attributes", %{board: board} do
16 | changeset = List.changeset(%List{board_id: board.id}, @valid_attrs)
17 |
18 | assert changeset.valid?
19 |
20 | list = Repo.insert!(changeset)
21 | assert list.position == 1024
22 | end
23 |
24 | test "changeset with invalid attributes", %{board: board} do
25 | changeset = List.changeset(%List{board_id: board.id}, @invalid_attrs)
26 |
27 | refute changeset.valid?
28 | end
29 |
30 | test "existing lists for the same board", %{board: board} do
31 | count = 3
32 |
33 | for i <- 1..(count-1) do
34 | board
35 | |> build_assoc(:lists)
36 | |> List.changeset(%{name: "List #{i}"})
37 | |> Repo.insert
38 | end
39 |
40 | {:ok, last_list} = board
41 | |> build_assoc(:lists)
42 | |> List.changeset(%{name: "Last"})
43 | |> Repo.insert
44 |
45 | assert last_list.position == 1024 * count
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/test/models/user_board_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.UserBoardTest do
2 | use PhoenixTrello.ModelCase
3 |
4 | import PhoenixTrello.Factory
5 |
6 | alias PhoenixTrello.UserBoard
7 |
8 | @valid_attrs %{}
9 | @invalid_attrs %{}
10 |
11 | setup do
12 | user = insert(:user)
13 | board = insert(:board)
14 |
15 | {:ok, user: user, board: board}
16 | end
17 |
18 | test "changeset with valid attributes", %{user: user, board: board} do
19 | attributes = @valid_attrs
20 | |> Map.put(:user_id, user.id)
21 | |> Map.put(:board_id, board.id)
22 |
23 | changeset = UserBoard.changeset(%UserBoard{}, attributes)
24 | assert changeset.valid?
25 | end
26 |
27 | test "changeset with invalid attributes", _ do
28 | changeset = UserBoard.changeset(%UserBoard{}, @invalid_attrs)
29 | refute changeset.valid?
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/test/models/user_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.UserTest do
2 | use PhoenixTrello.ModelCase
3 |
4 | alias PhoenixTrello.User
5 |
6 | @valid_attrs %{
7 | encrypted_password: "some content",
8 | email: "email@email.com",
9 | first_name: "some content",
10 | last_name: "some content",
11 | password: "123456"
12 | }
13 | @invalid_attrs %{}
14 |
15 | test "changeset with valid attributes" do
16 | changeset = User.changeset(%User{}, @valid_attrs)
17 | assert changeset.valid?
18 | end
19 |
20 | test "changeset with invalid attributes" do
21 | changeset = User.changeset(%User{}, @invalid_attrs)
22 | refute changeset.valid?
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | imports other functionality to make it easier
8 | to build and query models.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with channels
21 | use Phoenix.ChannelTest
22 |
23 | alias PhoenixTrello.Repo
24 | import Ecto.Model
25 | import Ecto.Query, only: [from: 2]
26 |
27 |
28 | # The default endpoint for testing
29 | @endpoint PhoenixTrello.Endpoint
30 | end
31 | end
32 |
33 | setup tags do
34 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(PhoenixTrello.Repo)
35 |
36 | unless tags[:async] do
37 | Ecto.Adapters.SQL.Sandbox.mode(PhoenixTrello.Repo, {:shared, self()})
38 | end
39 |
40 | :ok
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | imports other functionality to make it easier
8 | to build and query models.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with connections
21 | use Phoenix.ConnTest
22 |
23 | alias PhoenixTrello.Repo
24 | import Ecto.Model, except: [build: 2]
25 | import Ecto.Query, only: [from: 2]
26 |
27 | import PhoenixTrello.Router.Helpers
28 |
29 | import PhoenixTrello.Factory
30 |
31 | # The default endpoint for testing
32 | @endpoint PhoenixTrello.Endpoint
33 | end
34 | end
35 |
36 | setup tags do
37 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(PhoenixTrello.Repo)
38 |
39 | unless tags[:async] do
40 | Ecto.Adapters.SQL.Sandbox.mode(PhoenixTrello.Repo, {:shared, self()})
41 | end
42 |
43 | {:ok, conn: Phoenix.ConnTest.build_conn()}
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/test/support/factory.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Factory do
2 | use ExMachina.Ecto, repo: PhoenixTrello.Repo
3 |
4 | alias PhoenixTrello.{User, Board, List, Card}
5 |
6 | def user_factory do
7 | %User{
8 | first_name: sequence(:first_name, &"First #{&1}"),
9 | last_name: sequence(:last_name, &"Last #{&1}"),
10 | email: sequence(:email, &"email-#{&1}@foo.com"),
11 | encrypted_password: "12345678"
12 | }
13 | end
14 |
15 | def board_factory do
16 | %Board{
17 | name: sequence(:name, &"Name #{&1}"),
18 | user: build(:user)
19 | }
20 | end
21 |
22 | def board_with_lists_factory do
23 | %Board{
24 | name: sequence(:name, &"Name #{&1}"),
25 | user: build(:user),
26 | lists: build_list(3, :list)
27 | }
28 | end
29 |
30 | def list_factory do
31 | %List{
32 | name: sequence(:name, &"Name #{&1}")
33 | }
34 | end
35 |
36 | def card_factory do
37 | %Card{
38 | name: sequence(:name, &"Name #{&1}")
39 | }
40 | end
41 |
42 | def list_with_cards_factory do
43 | %List{
44 | name: sequence(:name, &"Name #{&1}"),
45 | board: build(:board),
46 | cards: build_list(5, :card)
47 | }
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/test/support/integration_case.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.IntegrationCase do
2 | use ExUnit.CaseTemplate
3 | use Hound.Helpers
4 | import PhoenixTrello.Factory
5 |
6 | alias PhoenixTrello.{Repo, User, Board, UserBoard}
7 |
8 | using do
9 | quote do
10 | use Hound.Helpers
11 |
12 | import Ecto, only: [build_assoc: 2]
13 | import Ecto.Model
14 | import Ecto.Query, only: [from: 2]
15 | import PhoenixTrello.Router.Helpers
16 | import PhoenixTrello.Factory
17 | import PhoenixTrello.IntegrationCase
18 |
19 | alias PhoenixTrello.Repo
20 |
21 | # The default endpoint for testing
22 | @endpoint PhoenixTrello.Endpoint
23 |
24 | hound_session
25 | end
26 | end
27 |
28 | setup tags do
29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(PhoenixTrello.Repo)
30 |
31 | unless tags[:async] do
32 | Ecto.Adapters.SQL.Sandbox.mode(PhoenixTrello.Repo, {:shared, self()})
33 | end
34 |
35 | :ok
36 | end
37 |
38 | def create_user do
39 | build(:user)
40 | |> User.changeset(%{password: "12345678"})
41 | |> Repo.insert!
42 | end
43 |
44 | def create_board(user) do
45 | board = user
46 | |> Ecto.build_assoc(:owned_boards)
47 | |> Board.changeset(%{name: "My new board"})
48 | |> Repo.insert!
49 |
50 | board
51 | |> Ecto.build_assoc(:user_boards)
52 | |> UserBoard.changeset(%{user_id: user.id})
53 | |> Repo.insert!
54 |
55 | board
56 | |> Repo.preload([:user, :members, lists: :cards])
57 | end
58 |
59 | def user_sign_in(%{user: user}) do
60 | navigate_to "/"
61 |
62 | sign_in_form = find_element(:id, "sign_in_form")
63 |
64 | sign_in_form
65 | |> find_within_element(:id, "user_email")
66 | |> fill_field(user.email)
67 |
68 | sign_in_form
69 | |> find_within_element(:id, "user_password")
70 | |> fill_field(user.password)
71 |
72 | sign_in_form
73 | |> find_within_element(:css, "button")
74 | |> click
75 |
76 | assert element_displayed?({:id, "authentication_container"})
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/test/support/model_case.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.ModelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | model tests.
5 |
6 | You may define functions here to be used as helpers in
7 | your model tests. See `errors_on/2`'s definition as reference.
8 |
9 | Finally, if the test case interacts with the database,
10 | it cannot be async. For this reason, every test runs
11 | inside a transaction which is reset at the beginning
12 | of the test unless the test case is marked as async.
13 | """
14 |
15 | use ExUnit.CaseTemplate
16 |
17 | using do
18 | quote do
19 | alias PhoenixTrello.Repo
20 | import Ecto, only: [build_assoc: 2]
21 | import Ecto.Model, except: [build: 2]
22 | import Ecto.Query, only: [from: 2]
23 | import PhoenixTrello.ModelCase
24 | end
25 | end
26 |
27 | setup tags do
28 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(PhoenixTrello.Repo)
29 |
30 | unless tags[:async] do
31 | Ecto.Adapters.SQL.Sandbox.mode(PhoenixTrello.Repo, {:shared, self()})
32 | end
33 |
34 | :ok
35 | end
36 |
37 | @doc """
38 | Helper for returning list of errors in model when passed certain data.
39 |
40 | ## Examples
41 |
42 | Given a User model that lists `:name` as a required field and validates
43 | `:password` to be safe, it would return:
44 |
45 | iex> errors_on(%User{}, %{password: "password"})
46 | [password: "is unsafe", name: "is blank"]
47 |
48 | You could then write your assertion like:
49 |
50 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"})
51 |
52 | You can also create the changeset manually and retrieve the errors
53 | field directly:
54 |
55 | iex> changeset = User.changeset(%User{}, password: "password")
56 | iex> {:password, "is unsafe"} in changeset.errors
57 | true
58 | """
59 | def errors_on(struct, data) do
60 | struct.__struct__.changeset(struct, data)
61 | |> Ecto.Changeset.traverse_errors(&PhoenixTrello.ErrorHelpers.translate_error/1)
62 | |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end)
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | Application.ensure_all_started(:hound)
2 |
3 | ExUnit.start
4 |
5 | Ecto.Adapters.SQL.Sandbox.mode(PhoenixTrello.Repo, :manual)
6 |
--------------------------------------------------------------------------------
/test/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.ErrorViewTest do
2 | use PhoenixTrello.ConnCase
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(PhoenixTrello.ErrorView, "404.html", []) ==
9 | "Page not found"
10 | end
11 |
12 | test "render 500.html" do
13 | assert render_to_string(PhoenixTrello.ErrorView, "500.html", []) ==
14 | "Server internal error"
15 | end
16 |
17 | test "render any other" do
18 | assert render_to_string(PhoenixTrello.ErrorView, "505.html", []) ==
19 | "Server internal error"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.LayoutViewTest do
2 | use PhoenixTrello.ConnCase
3 | end
--------------------------------------------------------------------------------
/test/views/page_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.PageViewTest do
2 | use PhoenixTrello.ConnCase
3 | end
4 |
--------------------------------------------------------------------------------
/web/channels/board_channel.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.BoardChannel do
2 | @moduledoc """
3 | Board channel
4 | """
5 |
6 | use PhoenixTrello.Web, :channel
7 |
8 | alias PhoenixTrello.{User, Board, UserBoard, List, Card, Comment, CardMember}
9 | alias PhoenixTrello.BoardChannel.Monitor
10 |
11 | def join("boards:" <> board_id, _params, socket) do
12 | current_user = socket.assigns.current_user
13 | board = get_current_board(socket, board_id)
14 |
15 | Monitor.create(board_id)
16 |
17 | send(self, {:after_join, Monitor.user_joined(board_id, current_user.id)})
18 |
19 | {:ok, %{board: board}, assign(socket, :board, board)}
20 | end
21 |
22 | def handle_info({:after_join, connected_users}, socket) do
23 | broadcast! socket, "user:joined", %{users: connected_users}
24 | {:noreply, socket}
25 | end
26 |
27 | def handle_in("lists:create", %{"list" => list_params}, socket) do
28 | board = socket.assigns.board
29 |
30 | changeset = board
31 | |> build_assoc(:lists)
32 | |> List.changeset(list_params)
33 |
34 | case Repo.insert(changeset) do
35 | {:ok, list} ->
36 | list = Repo.preload(list, [:board, :cards])
37 |
38 | broadcast! socket, "list:created", %{list: list}
39 | {:noreply, socket}
40 | {:error, _changeset} ->
41 | {:reply, {:error, %{error: "Error creating list"}}, socket}
42 | end
43 | end
44 |
45 | def handle_in("cards:create", %{"card" => card_params}, socket) do
46 | board = socket.assigns.board
47 | changeset = board
48 | |> assoc(:lists)
49 | |> Repo.get!(card_params["list_id"])
50 | |> build_assoc(:cards)
51 | |> Card.changeset(card_params)
52 |
53 | case Repo.insert(changeset) do
54 | {:ok, card} ->
55 | card = board
56 | |> assoc(:cards)
57 | |> Card.preload_all
58 | |> Repo.get!(card.id)
59 |
60 | broadcast! socket, "card:created", %{card: card}
61 | {:noreply, socket}
62 | {:error, _changeset} ->
63 | {:reply, {:error, %{error: "Error creating card"}}, socket}
64 | end
65 | end
66 |
67 | def handle_in("members:add", %{"email" => email}, socket) do
68 | try do
69 | board = socket.assigns.board
70 | user = User
71 | |> Repo.get_by(email: email)
72 |
73 | changeset = user
74 | |> build_assoc(:user_boards)
75 | |> UserBoard.changeset(%{board_id: board.id})
76 |
77 | case Repo.insert(changeset) do
78 | {:ok, _board_user} ->
79 | broadcast! socket, "member:added", %{user: user}
80 |
81 | PhoenixTrello.Endpoint.broadcast_from! self(), "users:#{user.id}", "boards:add", %{board: board}
82 |
83 | {:noreply, socket}
84 | {:error, _changeset} ->
85 | {:reply, {:error, %{error: "Error adding new member"}}, socket}
86 | end
87 | catch
88 | _, _-> {:reply, {:error, %{error: "User does not exist"}}, socket}
89 | end
90 | end
91 |
92 | def handle_in("card:update", %{"card" => card_params}, socket) do
93 | card = socket.assigns.board
94 | |> assoc(:cards)
95 | |> Repo.get!(card_params["id"])
96 |
97 | changeset = Card.update_changeset(card, card_params)
98 |
99 | case Repo.update(changeset) do
100 | {:ok, card} ->
101 | board = get_current_board(socket)
102 |
103 | card = Card
104 | |> Card.preload_all
105 | |> Repo.get(card.id)
106 |
107 | broadcast! socket, "card:updated", %{board: board, card: card}
108 | {:noreply, socket}
109 | {:error, _changeset} ->
110 | {:reply, {:error, %{error: "Error updating card"}}, socket}
111 | end
112 | end
113 |
114 | def handle_in("list:update", %{"list" => list_params}, socket) do
115 | list = socket.assigns.board
116 | |> assoc(:lists)
117 | |> Repo.get!(list_params["id"])
118 |
119 | changeset = List.update_changeset(list, list_params)
120 |
121 | case Repo.update(changeset) do
122 | {:ok, _list} ->
123 | board = get_current_board(socket)
124 | broadcast! socket, "list:updated", %{board: board}
125 | {:noreply, socket}
126 | {:error, _changeset} ->
127 | {:reply, {:error, %{error: "Error updating list"}}, socket}
128 | end
129 | end
130 |
131 | def handle_in("card:add_comment", %{"card_id" => card_id, "text" => text}, socket) do
132 | current_user = socket.assigns.current_user
133 |
134 | comment = socket.assigns.board
135 | |> assoc(:cards)
136 | |> Repo.get!(card_id)
137 | |> build_assoc(:comments)
138 |
139 | changeset = Comment.changeset(comment, %{text: text, user_id: current_user.id})
140 |
141 | case Repo.insert(changeset) do
142 | {:ok, _comment} ->
143 | card = Card
144 | |> Card.preload_all
145 | |> Repo.get(card_id)
146 |
147 | broadcast! socket, "comment:created", %{board: get_current_board(socket), card: card}
148 | {:noreply, socket}
149 | {:error, _changeset} ->
150 | {:reply, {:error, %{error: "Error creating comment"}}, socket}
151 | end
152 | end
153 |
154 | def handle_in("card:add_member", %{"card_id" => card_id, "user_id" => user_id}, socket) do
155 | try do
156 | current_board = socket.assigns.board
157 |
158 | card_member = current_board
159 | |> assoc(:cards)
160 | |> Repo.get!(card_id)
161 | |> build_assoc(:card_members)
162 |
163 | user_board = UserBoard
164 | |> UserBoard.find_by_user_and_board(user_id, current_board.id)
165 | |> Repo.one!()
166 |
167 | changeset = CardMember.changeset(card_member, %{user_board_id: user_board.id})
168 |
169 | case Repo.insert(changeset) do
170 | {:ok, _} ->
171 | card = Card
172 | |> Card.preload_all
173 | |> Repo.get(card_id)
174 |
175 | broadcast! socket, "card:updated", %{board: get_current_board(socket), card: card}
176 | {:noreply, socket}
177 | {:error, _} ->
178 | {:reply, {:error, %{error: "Error adding new member"}}, socket}
179 | end
180 | catch
181 | _, _-> {:reply, {:error, %{error: "Member does not exist"}}, socket}
182 | end
183 | end
184 |
185 | def handle_in("card:remove_member", %{"card_id" => card_id, "user_id" => user_id}, socket) do
186 | current_board = socket.assigns.board
187 |
188 | user_board = UserBoard
189 | |> UserBoard.find_by_user_and_board(user_id, current_board.id)
190 | |> Repo.one!
191 |
192 | card_member = CardMember
193 | |> CardMember.get_by_card_and_user_board(card_id, user_board.id)
194 | |> Repo.one!
195 |
196 | case Repo.delete(card_member) do
197 | {:ok, _} ->
198 | card = Card
199 | |> Card.preload_all
200 | |> Repo.get(card_id)
201 |
202 | broadcast! socket, "card:updated", %{board: get_current_board(socket), card: card}
203 | {:noreply, socket}
204 | {:error, _changeset} ->
205 | {:reply, {:error, %{error: "Error creating comment"}}, socket}
206 | end
207 | end
208 |
209 | def terminate(_reason, socket) do
210 | board_id = Board.slug_id(socket.assigns.board)
211 | user_id = socket.assigns.current_user.id
212 |
213 | broadcast! socket, "user:left", %{users: Monitor.user_left(board_id, user_id)}
214 |
215 | :ok
216 | end
217 |
218 | defp get_current_board(socket, board_id) do
219 | socket.assigns.current_user
220 | |> assoc(:boards)
221 | |> Board.preload_all
222 | |> Repo.get(board_id)
223 | end
224 |
225 | defp get_current_board(socket), do: get_current_board(socket, socket.assigns.board.id)
226 | end
227 |
--------------------------------------------------------------------------------
/web/channels/user_channel.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.UserChannel do
2 | use PhoenixTrello.Web, :channel
3 |
4 | def join("users:" <> user_id, _params, socket) do
5 | current_user = socket.assigns.current_user
6 |
7 | if String.to_integer(user_id) == current_user.id do
8 | {:ok, socket}
9 | else
10 | {:error, %{reason: "Invalid user"}}
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.UserSocket do
2 | use Phoenix.Socket
3 |
4 | alias PhoenixTrello.{GuardianSerializer}
5 |
6 | # Channels
7 | channel "boards:*", PhoenixTrello.BoardChannel
8 | channel "users:*", PhoenixTrello.UserChannel
9 |
10 | # Transports
11 | transport :websocket, Phoenix.Transports.WebSocket
12 | transport :longpoll, Phoenix.Transports.LongPoll
13 |
14 | def connect(%{"token" => token}, socket) do
15 | case Guardian.decode_and_verify(token) do
16 | {:ok, claims} ->
17 | case GuardianSerializer.from_token(claims["sub"]) do
18 | {:ok, user} ->
19 | {:ok, assign(socket, :current_user, user)}
20 | {:error, _reason} ->
21 | :error
22 | end
23 | {:error, _reason} ->
24 | :error
25 | end
26 | end
27 |
28 | def connect(_params, _socket), do: :error
29 |
30 | def id(socket), do: "users_socket:#{socket.assigns.current_user.id}"
31 | end
32 |
--------------------------------------------------------------------------------
/web/controllers/api/v1/board_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.BoardController do
2 | use PhoenixTrello.Web, :controller
3 |
4 | plug Guardian.Plug.EnsureAuthenticated, handler: PhoenixTrello.SessionController
5 | plug :scrub_params, "board" when action in [:create]
6 |
7 | alias PhoenixTrello.{Repo, Board, UserBoard}
8 |
9 | def index(conn, _params) do
10 | current_user = Guardian.Plug.current_resource(conn)
11 |
12 | owned_boards = current_user
13 | |> assoc(:owned_boards)
14 | |> Board.preload_all
15 | |> Repo.all
16 |
17 | invited_boards = current_user
18 | |> assoc(:boards)
19 | |> Board.not_owned_by(current_user.id)
20 | |> Board.preload_all
21 | |> Repo.all
22 |
23 | render(conn, "index.json", owned_boards: owned_boards, invited_boards: invited_boards)
24 | end
25 |
26 | def create(conn, %{"board" => board_params}) do
27 | current_user = Guardian.Plug.current_resource(conn)
28 |
29 | changeset = current_user
30 | |> build_assoc(:owned_boards)
31 | |> Board.changeset(board_params)
32 |
33 | if changeset.valid? do
34 | board = Repo.insert!(changeset)
35 |
36 | board
37 | |> build_assoc(:user_boards)
38 | |> UserBoard.changeset(%{user_id: current_user.id})
39 | |> Repo.insert!
40 |
41 | conn
42 | |> put_status(:created)
43 | |> render("show.json", board: board )
44 | else
45 | conn
46 | |> put_status(:unprocessable_entity)
47 | |> render("error.json", changeset: changeset)
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/web/controllers/api/v1/card_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.CardController do
2 | use PhoenixTrello.Web, :controller
3 |
4 | plug Guardian.Plug.EnsureAuthenticated, handler: PhoenixTrello.SessionController
5 |
6 | alias PhoenixTrello.{Repo, Card}
7 |
8 | def show(conn, %{"board_id" => board_id, "id" => card_id}) do
9 | card = Card
10 | |> Card.get_by_user_and_board(card_id, current_user(conn).id, board_id)
11 | |> Repo.one!
12 |
13 | render(conn, "show.json", card: card)
14 | end
15 |
16 | defp current_user(conn) do
17 | Guardian.Plug.current_resource(conn)
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/web/controllers/api/v1/current_user_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.CurrentUserController do
2 | use PhoenixTrello.Web, :controller
3 |
4 | plug Guardian.Plug.EnsureAuthenticated, handler: PhoenixTrello.SessionController
5 |
6 | def show(conn, _) do
7 | user = Guardian.Plug.current_resource(conn)
8 |
9 | conn
10 | |> put_status(:ok)
11 | |> render("show.json", user: user)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/web/controllers/api/v1/registration_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.RegistrationController do
2 | use PhoenixTrello.Web, :controller
3 |
4 | alias PhoenixTrello.{Repo, User}
5 |
6 | plug :scrub_params, "user" when action in [:create]
7 |
8 | def create(conn, %{"user" => user_params}) do
9 | changeset = User.changeset(%User{}, user_params)
10 |
11 | case Repo.insert(changeset) do
12 | {:ok, user} ->
13 | {:ok, jwt, _full_claims} = user |> Guardian.encode_and_sign(:token)
14 |
15 | conn
16 | |> put_status(:created)
17 | |> render(PhoenixTrello.SessionView, "show.json", jwt: jwt, user: user)
18 |
19 | {:error, changeset} ->
20 | conn
21 | |> put_status(:unprocessable_entity)
22 | |> render("error.json", changeset: changeset)
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/web/controllers/api/v1/session_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.SessionController do
2 | use PhoenixTrello.Web, :controller
3 |
4 | plug :scrub_params, "session" when action in [:create]
5 |
6 | def create(conn, %{"session" => session_params}) do
7 | case PhoenixTrello.Session.authenticate(session_params) do
8 | {:ok, user} ->
9 | {:ok, jwt, _full_claims} = user |> Guardian.encode_and_sign(:token)
10 |
11 | conn
12 | |> put_status(:created)
13 | |> render("show.json", jwt: jwt, user: user)
14 |
15 | :error ->
16 | conn
17 | |> put_status(:unprocessable_entity)
18 | |> render("error.json")
19 | end
20 | end
21 |
22 | def delete(conn, _) do
23 | {:ok, claims} = Guardian.Plug.claims(conn)
24 |
25 | conn
26 | |> Guardian.Plug.current_token
27 | |> Guardian.revoke!(claims)
28 |
29 | conn
30 | |> render("delete.json")
31 | end
32 |
33 | def unauthenticated(conn, _params) do
34 | conn
35 | |> put_status(:forbidden)
36 | |> render(PhoenixTrello.SessionView, "forbidden.json", error: "Not Authenticated")
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.PageController do
2 | use PhoenixTrello.Web, :controller
3 |
4 | def index(conn, _params) do
5 | render conn, "index.html"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/web/helpers/session.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Session do
2 | alias PhoenixTrello.{Repo, User}
3 |
4 | def authenticate(%{"email" => email, "password" => password}) do
5 | user = Repo.get_by(User, email: String.downcase(email))
6 |
7 | case check_password(user, password) do
8 | true -> {:ok, user}
9 | _ -> :error
10 | end
11 | end
12 |
13 | defp check_password(user, password) do
14 | case user do
15 | nil -> Comeonin.Bcrypt.dummy_checkpw()
16 | _ -> Comeonin.Bcrypt.checkpw(password, user.encrypted_password)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/web/models/board.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Board do
2 | use PhoenixTrello.Web, :model
3 |
4 | alias __MODULE__
5 | alias PhoenixTrello.{Repo, Permalink, List, Comment, Card, UserBoard, User}
6 |
7 | @primary_key {:id, Permalink, autogenerate: true}
8 |
9 | schema "boards" do
10 | field :name, :string
11 | field :slug, :string
12 |
13 | belongs_to :user, User
14 | has_many :lists, List
15 | has_many :cards, through: [:lists, :cards]
16 | has_many :user_boards, UserBoard
17 | has_many :members, through: [:user_boards, :user]
18 |
19 | timestamps
20 | end
21 |
22 | @required_fields ~w(name user_id)
23 | @optional_fields ~w(slug)
24 |
25 | @doc """
26 | Creates a changeset based on the `model` and `params`.
27 |
28 | If no params are provided, an invalid changeset is returned
29 | with no validation performed.
30 | """
31 | def changeset(model, params \\ %{}) do
32 | model
33 | |> cast(params, @required_fields, @optional_fields)
34 | |> slugify_name()
35 | end
36 |
37 | def not_owned_by(query \\ %Board{}, user_id) do
38 | from b in query,
39 | where: b.user_id != ^user_id
40 | end
41 |
42 | def preload_all(query) do
43 | comments_query = from c in Comment, order_by: [desc: c.inserted_at], preload: :user
44 | cards_query = from c in Card, order_by: c.position, preload: [[comments: ^comments_query], :members]
45 | lists_query = from l in List, order_by: l.position, preload: [cards: ^cards_query]
46 |
47 | from b in query, preload: [:user, :members, lists: ^lists_query]
48 | end
49 |
50 | def slug_id(board) do
51 | "#{board.id}-#{board.slug}"
52 | end
53 |
54 | defp slugify_name(current_changeset) do
55 | if name = get_change(current_changeset, :name) do
56 | put_change(current_changeset, :slug, slugify(name))
57 | else
58 | current_changeset
59 | end
60 | end
61 |
62 | defp slugify(value) do
63 | value
64 | |> String.downcase()
65 | |> String.replace(~r/[^\w-]+/, "-")
66 | end
67 | end
68 |
69 | defimpl Phoenix.Param, for: Board do
70 | def to_param(%{slug: slug, id: id}) do
71 | "#{id}-#{slug}"
72 | end
73 | end
74 |
75 | defimpl Poison.Encoder, for: PhoenixTrello.Board do
76 | def encode(model, options) do
77 | model
78 | |> Map.take([:name, :lists, :user, :members])
79 | |> Map.put(:id, PhoenixTrello.Board.slug_id(model))
80 | |> Poison.Encoder.encode(options)
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/web/models/card.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Card do
2 | use PhoenixTrello.Web, :model
3 |
4 | alias PhoenixTrello.{Repo, List, Card, Comment, CardMember}
5 |
6 | @derive {Poison.Encoder, only: [:id, :list_id, :name, :description, :position, :comments, :tags, :members]}
7 |
8 | schema "cards" do
9 | field :name, :string
10 | field :description, :string
11 | field :position, :integer
12 | field :tags, {:array, :string}
13 |
14 | belongs_to :list, List
15 | has_many :comments, Comment
16 | has_many :card_members, CardMember
17 | has_many :members, through: [:card_members, :user]
18 |
19 | timestamps
20 | end
21 |
22 | @required_fields ~w(name list_id)
23 | @optional_fields ~w(description position tags)
24 |
25 | @doc """
26 | Creates a changeset based on the `model` and `params`.
27 |
28 | If no params are provided, an invalid changeset is returned
29 | with no validation performed.
30 | """
31 | def changeset(model, params \\ %{}) do
32 | model
33 | |> cast(params, @required_fields, @optional_fields)
34 | |> calculate_position()
35 | end
36 |
37 | def update_changeset(model, params \\ %{}) do
38 | model
39 | |> cast(params, @required_fields, @optional_fields)
40 | end
41 |
42 | defp calculate_position(current_changeset) do
43 | model = current_changeset.data
44 |
45 | query = from(c in Card,
46 | select: c.position,
47 | where: c.list_id == ^(model.list_id),
48 | order_by: [desc: c.position],
49 | limit: 1)
50 |
51 | case Repo.one(query) do
52 | nil -> put_change(current_changeset, :position, 1024)
53 | position -> put_change(current_changeset, :position, position + 1024)
54 | end
55 | end
56 |
57 | def preload_all(query \\ %Card{}) do
58 | comments_query = from c in Comment, order_by: [desc: c.inserted_at], preload: :user
59 |
60 | from c in query, preload: [:members, [comments: ^comments_query]]
61 | end
62 |
63 | def get_by_user_and_board(query \\ %Card{}, card_id, user_id, board_id) do
64 | from c in query,
65 | left_join: co in assoc(c, :comments),
66 | left_join: cu in assoc(co, :user),
67 | left_join: me in assoc(c, :members),
68 | join: l in assoc(c, :list),
69 | join: b in assoc(l, :board),
70 | join: ub in assoc(b, :user_boards),
71 | where: ub.user_id == ^user_id and b.id == ^board_id and c.id == ^card_id,
72 | preload: [comments: {co, user: cu }, members: me]
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/web/models/card_member.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.CardMember do
2 | use PhoenixTrello.Web, :model
3 |
4 | alias __MODULE__
5 |
6 | schema "card_members" do
7 | belongs_to :card, PhoenixTrello.Card
8 | belongs_to :user_board, PhoenixTrello.UserBoard
9 | has_one :user, through: [:user_board, :user]
10 |
11 | timestamps
12 | end
13 |
14 | @required_fields ~w(card_id user_board_id)
15 | @optional_fields ~w()
16 |
17 | @doc """
18 | Creates a changeset based on the `model` and `params`.
19 |
20 | If no params are provided, an invalid changeset is returned
21 | with no validation performed.
22 | """
23 | def changeset(model, params \\ %{}) do
24 | model
25 | |> cast(params, @required_fields, @optional_fields)
26 | |> unique_constraint(:user_board_id, name: :card_members_card_id_user_board_id_index)
27 | end
28 |
29 | def get_by_card_and_user_board(query \\ %CardMember{}, card_id, user_board_id) do
30 | from cm in query,
31 | where: cm.card_id == ^card_id and cm.user_board_id == ^user_board_id,
32 | limit: 1
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/web/models/comment.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Comment do
2 | use PhoenixTrello.Web, :model
3 |
4 | @derive {Poison.Encoder, only: [:id, :user, :card_id, :text, :inserted_at]}
5 |
6 | schema "comments" do
7 | field :text, :string
8 |
9 | belongs_to :user, PhoenixTrello.User
10 | belongs_to :card, PhoenixTrello.Card
11 |
12 | timestamps
13 | end
14 |
15 | @required_fields ~w(user_id card_id text)
16 | @optional_fields ~w()
17 |
18 | @doc """
19 | Creates a changeset based on the `model` and `params`.
20 |
21 | If no params are provided, an invalid changeset is returned
22 | with no validation performed.
23 | """
24 | def changeset(model, params \\ %{}) do
25 | model
26 | |> cast(params, @required_fields, @optional_fields)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/web/models/list.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.List do
2 | use PhoenixTrello.Web, :model
3 |
4 | alias PhoenixTrello.{Board, Repo, List, Card}
5 |
6 | @derive {Poison.Encoder, only: [:id, :board_id, :name, :position, :cards]}
7 |
8 | schema "lists" do
9 | field :name, :string
10 | field :position, :integer
11 |
12 | belongs_to :board, Board
13 | has_many :cards, Card
14 |
15 | timestamps
16 | end
17 |
18 | @required_fields ~w(name)
19 | @optional_fields ~w(position)
20 |
21 | @doc """
22 | Creates a changeset based on the `model` and `params`.
23 |
24 | If no params are provided, an invalid changeset is returned
25 | with no validation performed.
26 | """
27 | def changeset(model, params \\ %{}) do
28 | model
29 | |> cast(params, @required_fields, @optional_fields)
30 | |> calculate_position()
31 | end
32 |
33 | def update_changeset(model, params \\ %{}) do
34 | model
35 | |> cast(params, @required_fields, @optional_fields)
36 | end
37 |
38 | defp calculate_position(current_changeset) do
39 | model = current_changeset.data
40 |
41 | query = from(l in List,
42 | select: l.position,
43 | where: l.board_id == ^(model.board_id),
44 | order_by: [desc: l.position],
45 | limit: 1)
46 |
47 | case Repo.one(query) do
48 | nil -> put_change(current_changeset, :position, 1024)
49 | position -> put_change(current_changeset, :position, position + 1024)
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/web/models/user.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.User do
2 | use PhoenixTrello.Web, :model
3 |
4 | alias PhoenixTrello.{Board, UserBoard}
5 |
6 | @derive {Poison.Encoder, only: [:id, :first_name, :last_name, :email]}
7 |
8 | schema "users" do
9 | field :first_name, :string
10 | field :last_name, :string
11 | field :email, :string
12 | field :encrypted_password, :string
13 | field :password, :string, virtual: true
14 |
15 | has_many :owned_boards, Board
16 | has_many :user_boards, UserBoard
17 | has_many :boards, through: [:user_boards, :board]
18 |
19 | timestamps
20 | end
21 |
22 | @required_fields ~w(first_name last_name email password)
23 | @optional_fields ~w(encrypted_password)
24 |
25 | @doc """
26 | Creates a changeset based on the `model` and `params`.
27 |
28 | If no params are provided, an invalid changeset is returned
29 | with no validation performed.
30 | """
31 | def changeset(model, params \\ %{}) do
32 | model
33 | |> cast(params, @required_fields, @optional_fields)
34 | |> validate_format(:email, ~r/@/)
35 | |> validate_length(:password, min: 5)
36 | |> validate_confirmation(:password, message: "Password does not match")
37 | |> unique_constraint(:email, message: "Email already taken")
38 | |> generate_encrypted_password
39 | end
40 |
41 | defp generate_encrypted_password(current_changeset) do
42 | case current_changeset do
43 | %Ecto.Changeset{valid?: true, changes: %{password: password}} ->
44 | put_change(current_changeset, :encrypted_password, Comeonin.Bcrypt.hashpwsalt(password))
45 | _ ->
46 | current_changeset
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/web/models/user_board.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.UserBoard do
2 | use PhoenixTrello.Web, :model
3 |
4 | alias __MODULE__
5 | alias PhoenixTrello.{User, Board}
6 |
7 | schema "user_boards" do
8 | belongs_to :user, User
9 | belongs_to :board, Board
10 |
11 | timestamps
12 | end
13 |
14 | @required_fields ~w(user_id board_id)
15 | @optional_fields ~w()
16 |
17 | @doc """
18 | Creates a changeset based on the `model` and `params`.
19 |
20 | If no params are provided, an invalid changeset is returned
21 | with no validation performed.
22 | """
23 | def changeset(model, params \\ %{}) do
24 | model
25 | |> cast(params, @required_fields, @optional_fields)
26 | |> unique_constraint(:user_id, name: :user_boards_user_id_board_id_index)
27 | end
28 |
29 | def find_by_user_and_board(query \\ %UserBoard{}, user_id, board_id) do
30 | from u in query,
31 | where: u.user_id == ^user_id and u.board_id == ^board_id
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixTrello.Router do
2 | use PhoenixTrello.Web, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_flash
8 | plug :protect_from_forgery
9 | plug :put_secure_browser_headers
10 | end
11 |
12 | pipeline :api do
13 | plug :accepts, ["json"]
14 | plug Guardian.Plug.VerifyHeader
15 | plug Guardian.Plug.LoadResource
16 | end
17 |
18 | scope "/api", PhoenixTrello do
19 | pipe_through :api
20 |
21 | scope "/v1" do
22 | post "/registrations", RegistrationController, :create
23 |
24 | post "/sessions", SessionController, :create
25 | delete "/sessions", SessionController, :delete
26 |
27 | get "/current_user", CurrentUserController, :show
28 |
29 | resources "/boards", BoardController, only: [:index, :create] do
30 | resources "/cards", CardController, only: [:show]
31 | end
32 | end
33 | end
34 |
35 | scope "/", PhoenixTrello do
36 | pipe_through :browser # Use the default browser stack
37 |
38 | get "/*path", PageController, :index
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/web/static/css/application.sass:
--------------------------------------------------------------------------------
1 | // - - css-burrito v1.5 | mit license | github.com/jasonreece/css-burrito
2 |
3 | // - - - - - - - - - - - - - - - - - - -
4 | // - - application.scss
5 | // contains an import section for libs, global, and modules, the inbox,
6 | // and a shame section for quick fixes and hacks.
7 |
8 | // - - - - - - - - - - - - - - - - - - -
9 | // - - libs
10 | // add css frameworks and libraries here.
11 | // be sure to load them after the variable overrides file
12 | // if you want to change variable values.
13 |
14 | // - - variable overrides for sass libraries
15 | // @import "libs/library-variable-overrides";
16 |
17 | // - - reset - normalize.css v3.0.2
18 | @import libs/normalize
19 |
20 | @import ../../../node_modules/bourbon/app/assets/stylesheets/bourbon
21 | @import ../../../node_modules/bourbon-neat/app/assets/stylesheets/neat-helpers
22 | @import global/grid_settings
23 | @import ../../../node_modules/bourbon-neat/app/assets/stylesheets/neat
24 |
25 | // - - - - - - - - - - - - - - - - - - -
26 | // - - global
27 |
28 | // - - global/settings
29 | // @font-face declarations, variables
30 | @import global/settings
31 |
32 | // - - global/utilities
33 | // extends, functions, and mixins
34 | @import global/utilities
35 |
36 | // - - global/base
37 | // base-level tags (body, p, etc.)
38 | @import global/base
39 |
40 | // - - global/layout
41 | // margin, padding, sizing
42 | @import global/layout
43 |
44 | // - - global/skin
45 | // backgrounds, borders, box-shadow, etc
46 | @import global/skin
47 |
48 | // - - global/typography
49 | // fonts and colors
50 | @import global/typography
51 |
52 | // - - - - - - - - - - - - - - - - - - -
53 | // - - modules
54 | // add new modules to the modules/_modules.scss file and they'll get pulled in here.
55 | @import modules/modules
56 |
57 | // - - - - - - - - - - - - - - - - - - -
58 | // - - inbox
59 | // the inbox allows developers, and those not actively working on the project
60 | // to quickly add styles that are easily seen by the maintainer of the file.
61 |
62 | // - - - - - - - - - - - - - - - - - - -
63 | // - - shame
64 | // need to add a quick fix, hack, or questionable technique? add it here, fix it later.
65 |
--------------------------------------------------------------------------------
/web/static/css/global/_base.sass:
--------------------------------------------------------------------------------
1 | // - - - - - - - - - - - - - - - - - - -
2 | // - - base
3 | // project defaults for base elements - h1-h6, p, a, etc.
4 |
5 |
6 | h1, h2, h3
7 | color: #fff
8 |
--------------------------------------------------------------------------------
/web/static/css/global/_grid_settings.sass:
--------------------------------------------------------------------------------
1 | // Neat Overrides
2 | ///////////////////////////////////////////////////////////////////////////////
3 | // $column: 90px;
4 | // $gutter: 30px;
5 | // $grid-columns: 12;
6 | // $max-width: em(1088);
7 |
8 | // Neat Breakpoints
9 | ///////////////////////////////////////////////////////////////////////////////
10 | $medium-screen: em(1024)
11 | $large-screen: em(1366)
12 | $x-large-screen: em(1920)
13 |
14 | $small-screen-up: new-breakpoint(max-width ($medium-screen - em(1)))
15 | $medium-screen-up: new-breakpoint(min-width $medium-screen max-width ($large-screen - em(1)))
16 | $large-screen-up: new-breakpoint(min-width $large-screen max-width ($x-large-screen - em(1)))
17 | $x-large-screen-up: new-breakpoint(min-width $x-large-screen)
18 |
--------------------------------------------------------------------------------
/web/static/css/global/_layout.sass:
--------------------------------------------------------------------------------
1 | // - - - - - - - - - - - - - - - - - - -
2 | // - - layout
3 | // global layout classes - height, width, padding, margin, etc.
4 |
5 | html
6 | height: 100vh
7 | body
8 | height: 100vh
9 |
10 | #main_container
11 | height: calc(100% - 40px)
12 | > div
13 | height: 100%
14 |
15 | #main_footer
16 | text-align: center
17 | small
18 | line-height: 40px
19 | color: rgba(#fff, .5)
20 |
21 | a
22 | +transition
23 | color: $gray
24 | &:hover
25 | color: #fff
26 |
27 |
28 | .application-container
29 | height: 100vh
30 |
31 |
32 | .main-container
33 | +display(flex)
34 | height: calc(100% - 65px)
35 |
36 | .view-container
37 | flex: 1
38 | padding: 0 $base-spacing
39 | height: calc(100% - 3em)
40 |
41 | .view-header
42 | padding: $base-spacing/2 0
43 |
44 | h3
45 | margin-bottom: 0
46 |
--------------------------------------------------------------------------------
/web/static/css/global/_settings.sass:
--------------------------------------------------------------------------------
1 | // - - - - - - - - - - - - - - - - - - -
2 | // - - settings
3 | // @font-face declarations, variables
4 |
5 | // - - - - - - - - - - - - - - - - - - -
6 | // - - @font-face
7 | // add fonts to project as needed by updating the $fonts map below in the following format -
8 | // 'font-family-name': 'font-weight'
9 |
10 | // $fonts: (
11 | // 'font-family-one': ('font-one-light', 'font-one-dark'),
12 | // 'font-family-two': ('font-two-light','font-two-medium')
13 | // )
14 |
15 | // @each $font-family, $font-weights in $fonts {
16 | // @each $font-weight in $font-weights {
17 | // @font-face {
18 | // name: $font-weight
19 | // src:
20 | // url('/path/to/fonts/#{$font-family}/#{$font-weight}.eot?#iefix') format('embedded-opentype'),
21 | // url('/path/to/fonts/#{$font-family}/#{$font-weight}.woff') format('woff'),
22 | // url('/path/to/fonts/#{$font-family}/#{$font-weight}.ttf') format('truetype'),
23 | // url('/path/to/fonts/#{$font-family}/#{$font-weight}.svg##{$font-weight}') format('svg')
24 | // }
25 | // }
26 | // }
27 |
28 | // - - - - - - - - - - - - - - - - - - -
29 | // - - variables
30 | $base-line-height: 1.5
31 | $heading-line-height: 1.2em
32 | $base-border-radius: 3px
33 | $base-spacing: $base-line-height * 1em
34 | $small-spacing: $base-spacing / 2
35 | $base-z-index: 0
36 |
37 | // - - colors
38 | $blue: #3498db
39 | $green: #2ecc71
40 | $red: #e74c3c
41 | $yellow: #f1c40f
42 | $orange: #e67e22
43 | $purple: #89609E
44 | $gray: #bdc3c7
45 | $asphalt: #34495e
46 | $pink: #ff5aec
47 |
48 | $dark-blue: shade($blue, 15)
49 | $dark-purple: shade($purple, 15)
50 | $light-gray: tint($gray, 50)
51 | $lighter-gray: tint($gray, 20)
52 | $medium-gray: shade($gray, 10)
53 | $dark-gray: shade($gray, 60)
54 | $light-red: tint($red, 15)
55 | $light-yellow: tint($yellow, 15)
56 | $light-green: tint($green, 15)
57 | $dark-asphalt: shade($asphalt, 7)
58 | $light-asphalt: tint($asphalt, 30)
59 | $light-blue: tint($blue, 20)
60 | $dark-orange: shade($orange, 15)
61 | $dark-red: shade($red, 15)
62 | $light-pink: tint($pink, 31)
63 |
64 | $base-font-color: $dark-gray
65 | $action-color: $blue
66 |
67 | $base-body-color: $light-gray
68 |
69 | $base-link-color: $blue
70 | $hover-link-color: shade($base-link-color, 15)
71 |
72 | $base-border-color: $light-gray
73 | $base-border: 1px solid $base-border-color
74 |
75 | $base-background-color: #fff
76 | $secondary-background-color: tint($base-border-color, 75%)
77 |
78 | $form-box-shadow: none
79 | $form-box-shadow-focus: none
80 |
81 | // - - animations
82 | $base-duration: 150ms
83 | $base-timing: ease
84 |
85 | // - - typography
86 | $base-font-family: "Source Sans Pro", sans-serif
87 | $heading-font-family: 'Open Sans', sans-serif
88 | $base-font-size: 1em
89 |
90 | $tags-colors: (green: #61bd4f, yellow: #f2d600, orange: #ffab4a, red: #eb5a46, purple: #c377e0, blue: #0079bf)
91 |
92 | // - - grid
93 |
94 | +keyframes(fadeIn)
95 | from
96 | opacity: 0
97 | to
98 | opacity: 1
99 |
--------------------------------------------------------------------------------
/web/static/css/global/_skin.sass:
--------------------------------------------------------------------------------
1 | // - - - - - - - - - - - - - - - - - - -
2 | // - - skin
3 | // global skin styles - gradients, colors, box-shadows, etc.
4 |
5 | body
6 | background: $purple
7 |
8 |
9 |
10 | @each $key, $color in $tags-colors
11 | .tag.#{$key}
12 | background: $color
13 | &:hover, &.selected
14 | box-shadow: -8px 0 shade($color, 20)
15 |
--------------------------------------------------------------------------------
/web/static/css/global/_typography.sass:
--------------------------------------------------------------------------------
1 | // - - - - - - - - - - - - - - - - - - -
2 | // - - typography
3 | // global typography styles
4 | body
5 | color: $base-font-color
6 | font-family: $base-font-family
7 | font-size: $base-font-size
8 | line-height: $base-line-height
9 |
10 |
11 | h1,
12 | h2,
13 | h3,
14 | h4,
15 | h5,
16 | h6
17 | font-family: $heading-font-family
18 | line-height: $heading-line-height
19 | margin: 0 0 $small-spacing
20 |
21 |
22 | p
23 | margin: 0 0 $small-spacing
24 |
25 |
26 | a
27 | color: $action-color
28 | text-decoration: none
29 | transition: color $base-duration $base-timing
30 |
31 | &:active,
32 | &:focus,
33 | &:hover
34 | color: shade($action-color, 25%)
35 |
36 |
37 |
38 | hr
39 | border-bottom: $base-border
40 | border-left: 0
41 | border-right: 0
42 | border-top: 0
43 | margin: $base-spacing 0
44 |
45 |
46 | img,
47 | picture
48 | margin: 0
49 | max-width: 100%
50 |
--------------------------------------------------------------------------------
/web/static/css/global/_utilities.sass:
--------------------------------------------------------------------------------
1 | // - - - - - - - - - - - - - - - - - - -
2 | // - - utilities
3 | // placeholders extends, mixins, functions, and utility classes
4 |
5 | // - - - - - - - - - - - - - - - - - - -
6 | // - - placeholder extends
7 |
8 | // - - - - - - - - - - - - - - - - - - -
9 | // - - mixins
10 |
11 | // - - clearfix
12 | // @mixin clearfix {
13 | // &:before,
14 | // &:after {
15 | // content: ' ';
16 | // display: table;
17 | // }
18 | // &:after {
19 | // clear: both;
20 | // }
21 | // & {
22 | // *zoom: 1;
23 | // }
24 | // }
25 |
26 | // - - breakpoint
27 | // adds responsive breakpoints.
28 | // @mixin breakpoint($width) {
29 | // @media (min-width: $width) {
30 | // @content;
31 | // }
32 | // }
33 |
34 | // - - attention
35 | // adds accessibility pseudo selectors to hover states.
36 | // @mixin attention() {
37 | // &:hover,
38 | // &:active,
39 | // &:focus {
40 | // @content;
41 | // }
42 | // }
43 |
44 | // - - - - - - - - - - - - - - - - - - -
45 | // - - functions
46 |
47 | // - - - - - - - - - - - - - - - - - - -
48 | // - - utilities
49 | @each $color in (purple, orange, red, green)
50 | .#{$color}
51 | color: $color
52 |
--------------------------------------------------------------------------------
/web/static/css/libs/_library-variable-overrides.sass:
--------------------------------------------------------------------------------
1 | // - - - - - - - - - - - - - - - - - - -
2 | // - - library variable overrides
3 | // variable overrides for css frameworks and libraries
4 |
--------------------------------------------------------------------------------
/web/static/css/modules/_boards.sass:
--------------------------------------------------------------------------------
1 | .boards.index
2 | overflow-y: scroll
3 |
4 | .boards-wrapper
5 | $margin: 1em
6 |
7 | +display(flex)
8 | +flex-wrap(wrap)
9 | +flex-flow(row wrap)
10 | margin: 0 (-$margin/2) $margin (-$margin/2)
11 |
12 | .board
13 | +transition
14 | +flex(0 0 25%)
15 | cursor: pointer
16 |
17 | .inner
18 | background: rgba($light-gray, .9)
19 | border-radius: $base-border-radius
20 | margin: 0 $margin/2 $margin $margin/2
21 | line-height: 1.75em
22 | overflow: hidden
23 | padding: $base-spacing/2
24 | min-height: 5em
25 | animation-duration: .3s
26 | animation-name: fadeIn
27 |
28 |
29 | &.form
30 | input
31 | +fill-parent
32 | margin-bottom: .5em
33 |
34 | &.add-new
35 | .inner
36 | +transition
37 | background: rgba(#000, .1)
38 | cursor: pointer
39 | color: $gray
40 |
41 | &:hover
42 | background: rgba(#000, .2)
43 |
44 | .inner
45 | position: relative
46 |
47 | a
48 | +transition
49 | +transform(translate(-50%, -50%))
50 | display: block
51 | position: absolute
52 | top: 50%
53 | left: 50%
54 | color: $gray
55 |
56 | .boards.show
57 | .fa-spin
58 | color: #fff
59 | .view-header
60 | position: relative
61 |
62 | h3
63 | margin: 0
64 | color: #fff
65 |
66 | .board-users
67 | +transform(translateY(-50%))
68 | list-style: none
69 | margin: 0
70 | padding: 0
71 | position: absolute
72 | right: 0
73 | top: 50%
74 | z-index: 99
75 |
76 | > span > li
77 | display: inline-block
78 | margin-left: .5em
79 |
80 | &.connected
81 | opacity: 1
82 |
83 | .react-gravatar
84 | opacity: 1
85 |
86 | .react-gravatar, .add-new
87 | +transition
88 | border-radius: $base-border-radius
89 | width: 30px
90 | height: 30px
91 | opacity: .2
92 |
93 | .add-new
94 | +transition
95 | opacity: 1
96 | display: block
97 | position: relative
98 | border: 1px solid rgba(#fff, .5)
99 | color: rgba(#fff, .5)
100 | line-height: 1em
101 | font-size: .9em
102 |
103 | .fa
104 | +transform(translate(-50%, -50%))
105 | position: absolute
106 | top: 50%
107 | left: 50%
108 |
109 | &:hover
110 | color: #fff
111 | border-color: #fff
112 |
113 | .drop-down
114 | +transition
115 | position: absolute
116 | right: 0
117 | top: 100%
118 | list-style: none
119 | padding: $base-spacing/2
120 | background: rgba($light-gray, .9)
121 | border-radius: $base-border-radius
122 | min-width: 20em
123 | display: none
124 | animation-duration: .3s
125 | animation-name: fadeIn
126 |
127 | &.active
128 | display: block
129 |
130 | form
131 | input
132 | +fill-parent
133 | margin-bottom: .5em
134 |
135 | .avatar-appear
136 | opacity: 0.01
137 |
138 | .avatar-appear.avatar-appear-active
139 | +transition(opacity .5s ease-in)
140 | opacity: 1
141 |
142 | .avatar-enter
143 | opacity: 0.01
144 |
145 | .avatar-enter.avatar-enter-active
146 | +transition(opacity 500ms ease-in)
147 | opacity: 1
148 |
149 | .avatar-leave
150 | opacity: 1
151 |
152 | .avatar-leave.avatar-leave-active
153 | +transition(opacity 300ms ease-in)
154 | opacity: 0.01
155 |
156 | .canvas-wrapper
157 | position: relative
158 | height: calc(100% - 1em)
159 |
160 | .canvas
161 | position: absolute
162 | top: 0
163 | bottom: 0
164 | left: 0
165 | right: 0
166 | overflow-x: auto
167 | overflow-y: hidden
168 | white-space: nowrap
169 |
170 |
171 | .lists-wrapper
172 | $margin: 1em
173 | +clearfix
174 | margin: 0
175 | +display(flex)
176 |
177 | .list
178 | +flex(0 0 17em)
179 | margin: 0 $margin/1.5 0 0
180 | animation-duration: .3s
181 | animation-name: fadeIn
182 |
183 | > .inner
184 | background: rgba($light-gray, .9)
185 | border-radius: $base-border-radius
186 | padding: $base-spacing/2
187 | cursor: -webkit-grab
188 | cursor: -moz-grab
189 | cursor: grab
190 |
191 | .list.form
192 | +flex(none)
193 | +fill-parent
194 | display: block
195 | margin: 0 0 $base-spacing 0
196 |
197 | .inner
198 | padding: 0
199 |
200 | header
201 | +fill-parent
202 | cursor: pointer
203 | text-overflow: ellipsis
204 | white-space: nowrap
205 | overflow: hidden
206 |
207 | &.form
208 | display: inline-block
209 | input
210 | +fill-parent
211 | display: block
212 | margin-bottom: .5em
213 |
214 | &:not(.add-new)
215 | .inner .inner
216 | background: transparent
217 |
218 | &.add-new
219 | .inner
220 | +transition
221 | background: rgba(#000, .1)
222 | cursor: pointer
223 | color: $gray
224 |
225 | &:hover
226 | background: rgba(#000, .2)
227 | footer
228 | .add-new
229 | +transition
230 | display: block
231 | margin: -($base-spacing/2)
232 | margin-top: $base-spacing/2
233 | padding: .5em $base-spacing/2
234 | color: tint($dark-gray, 25)
235 | border-radius: 0 0 $base-border-radius $base-border-radius
236 |
237 | &:hover
238 | background: rgba($gray, .9)
239 | color: $dark-gray
240 |
241 | .card:not(.form)
242 | +transition
243 | margin-bottom: $base-spacing/3
244 | cursor: pointer
245 | animation-duration: .3s
246 | animation-name: fadeIn
247 | white-space: normal
248 |
249 | &.is-over
250 | padding-top: 50px
251 | position: relative
252 |
253 | :before
254 | background: rgba($dark-gray, .1)
255 | border-radius: $base-border-radius
256 | content: ""
257 | display: block
258 | height: 40px
259 | width: 100%
260 | position: absolute
261 | top: 0
262 | left: 0
263 | margin-bottom: $base-spacing/3
264 |
265 | .card-content
266 | background: #fff
267 | padding: $base-spacing/3
268 | border-radius: $base-border-radius
269 | border-bottom: 1px solid $gray
270 |
271 | .tags-wrapper
272 | +clearfix
273 |
274 | .tag
275 | +span-columns(2)
276 | animation-duration: .3s
277 | animation-name: fadeIn
278 | height: .5em
279 | border-radius: $base-border-radius
280 | &:hover
281 | box-shadow: 0 0 0
282 |
283 | .react-gravatar
284 | border-radius: $base-border-radius
285 | width: 30px
286 | height: 30px
287 | float: right
288 | margin-left: 5px
289 | animation-duration: .3s
290 | animation-name: fadeIn
291 |
292 | &:hover
293 | background: rgba($gray, .4)
294 |
295 | footer
296 | +clearfix
297 | small
298 | color: tint($dark-gray, 35)
299 |
300 | .card.form
301 | textarea
302 | +fill-parent
303 | animation-duration: .3s
304 | animation-name: fadeIn
305 | display: block
306 | margin-bottom: .5em
307 | border: 0px none
308 | border-bottom: 1px solid $gray
309 |
--------------------------------------------------------------------------------
/web/static/css/modules/_example-module.sass:
--------------------------------------------------------------------------------
1 | // - - - - - - - - - - - - - - - - - - -
2 | // - - example-module module
3 | // styles for the example-module module
4 |
--------------------------------------------------------------------------------
/web/static/css/modules/_forms.sass:
--------------------------------------------------------------------------------
1 | form, .form
2 | [type=text], [type=password], [type=date], [type=email], select, .selectize-input, textarea, [type=number], button
3 | border: 1px solid $gray
4 | padding: .75em
5 | border-radius: $base-border-radius
6 | box-shadow: none
7 | line-height: 1.2em
8 |
9 | &:focus
10 | box-shadow: none
11 | outline: none
12 |
13 | .error
14 | +animation(fadeIn .3s)
15 | color: $red
16 |
--------------------------------------------------------------------------------
/web/static/css/modules/_main_header.sass:
--------------------------------------------------------------------------------
1 | $header-height: $base-line-height * 1.8em
2 |
3 | .main-header
4 | +clearfix
5 | background: $dark-purple
6 | line-height: $header-height
7 | padding: 0 $base-spacing
8 | color: #fff
9 | position: relative
10 |
11 | .logo
12 | +transform(translate(-50%, -50%))
13 | display: inline-block
14 | background: url(/images/logo.png) 0 0 no-repeat
15 | background-size: contain
16 | height: 32px
17 | width: 110px
18 | position: absolute
19 | top: 50%
20 | left: 50%
21 |
22 |
23 | nav
24 | float: left
25 | margin: 0
26 | padding: 0
27 |
28 | &.right
29 | float: right
30 |
31 | ul
32 | list-style: none
33 | display: inline-block
34 | margin: 0
35 | -webkit-margin-before: 0
36 | -webkit-padding-start: 0
37 | li
38 | display: inline-block
39 |
40 | a
41 | +transition
42 | color: rgba(#fff, .5)
43 | background: rgba(#fff, .15)
44 | border-radius: $base-border-radius
45 | padding: .3em .5em
46 | line-height: 1em
47 |
48 | &.current-user
49 | position: relative
50 | overflow: hidden
51 | padding-left: 35px
52 | margin-right: .5em
53 |
54 | .react-gravatar
55 | width: 30px
56 | height: 30px
57 | border-radius: $base-border-radius
58 | margin-right: .5em
59 | position: absolute
60 | top: 50%
61 | margin-top: -15px
62 | left: 0
63 |
64 |
65 |
66 | .fa
67 | color: #fff
68 |
69 | &:hover
70 | color: #fff
71 |
72 | boards_nav
73 | > ul > li
74 | position: relative
75 |
76 | a
77 | display: block
78 |
79 | .dropdown
80 | position: absolute
81 | display: block
82 | z-index: 99
83 | background: #fff
84 | border-radius: $base-border-radius
85 | left: 0
86 | margin-top: .5em
87 | color: $base-font-color
88 | width: 15em
89 | padding: .3em
90 | box-shadow: 0 3px 6px rgba(0,0,0,.4)
91 | animation-duration: .3s
92 | animation-name: fadeIn
93 |
94 | header
95 | padding: 0 .5em
96 | color: $gray
97 |
98 | ul
99 | display: block
100 | position: relative
101 | max-height: 15em
102 | overflow-y: auto
103 |
104 | li
105 | display: block
106 | margin-bottom: .3em
107 |
108 | a
109 | +transition
110 | color: $base-font-color
111 | font-weight: bold
112 | display: block
113 | padding: .7em .5em
114 | background: rgba($purple, .2)
115 |
116 | &:hover
117 | background: rgba($purple, .5)
118 |
119 | &.options
120 | margin-top: 1em
121 | li
122 | a
123 | background: #fff
124 | text-decoration: underline
125 | font-weight: normal
126 | color: $dark-gray
127 |
128 | &:hover
129 | background: $light-gray
130 |
--------------------------------------------------------------------------------
/web/static/css/modules/_members_selector.sass:
--------------------------------------------------------------------------------
1 | .members-selector, .tags-selector
2 | background: #fff
3 | border: 1px solid $gray
4 | border-radius: $base-border-radius
5 | position: absolute
6 | padding: $base-spacing/2
7 | width: 18em
8 | animation-duration: .3s
9 | animation-name: fadeIn
10 |
11 | header
12 | border-bottom: 1px solid $light-gray
13 | margin-bottom: $base-spacing/2
14 | text-align: center
15 | position: relative
16 | padding-bottom: .5em
17 |
18 | > .close
19 | position: absolute
20 | right: 0
21 | top: 0
22 | line-height: 1em
23 | color: tint($dark-gray, 30)
24 |
25 | ul
26 | list-style: none
27 | padding: 0
28 | margin: 0
29 |
30 | li
31 | margin-bottom: 3px
32 |
33 | a
34 | +transition
35 | border-radius: $base-border-radius
36 | padding: 3px
37 | display: block
38 | position: relative
39 | color: $base-font-color
40 |
41 | &.tag
42 | height: 2em
43 |
44 | .fa
45 | color: #fff
46 |
47 | @each $key, $color in $tags-colors
48 | &.tag.#{$key}
49 | &:hover, &.selected
50 | box-shadow: -8px 0 shade($color, 20)
51 |
52 | .fa
53 | +transform(translateY(-50%))
54 | position: absolute
55 | right: $base-spacing/3
56 | top: 50%
57 |
58 | &:not(.tag):hover
59 | background: $blue
60 | color: #fff
61 |
62 | .react-gravatar
63 | border-radius: $base-border-radius
64 | width: 30px
65 | height: 30px
66 | vertical-align: middle
67 | margin-right: .5em
68 |
--------------------------------------------------------------------------------
/web/static/css/modules/_modals.sass:
--------------------------------------------------------------------------------
1 | .md-overlay
2 | position: fixed
3 | width: 100%
4 | height: 100%
5 | top: 0
6 | left: 0
7 | z-index: 1000
8 | background: rgba(#333, .7)
9 | overflow-y: scroll
10 | animation-duration: .3s
11 | animation-name: fadeIn
12 |
13 | .md-modal
14 | +backface-visibility(hidden)
15 | position: fixed
16 | pointer-events: none
17 | z-index: 2000
18 | top: 0
19 | left: 0
20 | right: 0
21 | bottom: 0
22 | overflow-y: scroll
23 |
24 | .md-content
25 | $width: 700px
26 | +pad($base-spacing)
27 | border-radius: $base-border-radius
28 | background: $light-gray
29 | position: absolute
30 | pointer-events: all
31 | width: $width
32 | top: $base-spacing*2
33 | left: 50%
34 | margin-left: -$width/2
35 | animation-duration: .3s
36 | animation-name: fadeIn
37 |
38 | > header
39 | margin-bottom: 1em
40 |
41 |
42 | .card-modal
43 | +clearfix
44 | position: relative
45 |
46 | .react-gravatar
47 | border-radius: $base-border-radius
48 | width: 30px
49 | height: 30px
50 |
51 | > .close
52 | position: absolute
53 | right: $base-spacing/2
54 | top: $base-spacing/2
55 | line-height: 1em
56 | color: tint($dark-gray, 30)
57 |
58 | &:hover
59 | color: $dark-gray
60 |
61 | .info
62 | +span-columns(9)
63 |
64 | .options
65 | +span-columns(3)
66 | position: relative
67 | overflow: visible
68 |
69 | .button
70 | +fill-parent
71 | +transition
72 | background: #fff
73 | padding: .3em .5em
74 | border-radius: $base-border-radius
75 | border-bottom: 1px solid $gray
76 | display: block
77 | color: $base-font-color
78 | font-weight: bold
79 | margin-bottom: $base-spacing/3
80 |
81 | .fa
82 | color: $gray
83 | margin-right: .5em
84 |
85 | &:hover
86 | background: $blue
87 | color: #fff
88 |
89 | .fa
90 | color: #fff
91 |
92 | header
93 | h3
94 | color: $base-font-color
95 |
96 |
97 | input, textarea
98 | +fill-parent
99 | display: block
100 | margin-bottom: .5em
101 | border: 0px none
102 | border-bottom: 1px solid $gray
103 |
104 | .form-wrapper
105 | +clearfix
106 | margin: $base-spacing 0
107 |
108 | .gravatar-wrapper
109 | +span-columns(1)
110 | .react-gravatar
111 | border-radius: $base-border-radius
112 | width: 30px
113 | height: 30px
114 |
115 | .form-controls
116 | +span-columns(11)
117 |
118 | .comment
119 | +clearfix
120 | margin-bottom: $base-spacing/2
121 | animation-duration: .3s
122 | animation-name: fadeIn
123 |
124 | .gravatar-wrapper
125 | +span-columns(1)
126 |
127 |
128 | .info-wrapper
129 | +span-columns(11)
130 | h5
131 | margin-bottom: .5em
132 | .text
133 | background: #fff
134 | border-radius: $base-border-radius
135 | padding: .5em
136 | border: 0px none
137 | border-bottom: 1px solid $gray
138 |
139 | .items-wrapper
140 | +clearfix
141 |
142 | .card-members
143 | +span-columns(6)
144 | margin-bottom: $base-spacing
145 | animation-duration: .3s
146 | animation-name: fadeIn
147 | .react-gravatar
148 | margin-right: 5px
149 | animation-duration: .3s
150 | animation-name: fadeIn
151 |
152 | .card-tags
153 | +span-columns(6)
154 | margin-bottom: $base-spacing
155 | animation-duration: .3s
156 | animation-name: fadeIn
157 | .tag
158 | width: 30px
159 | height: 30px
160 | margin-right: 5px
161 | box-shadow: 0 0 0
162 | display: inline-block
163 | border-radius: $base-border-radius
164 | animation-duration: .3s
165 | animation-name: fadeIn
166 |
--------------------------------------------------------------------------------
/web/static/css/modules/_modules.sass:
--------------------------------------------------------------------------------
1 | // - - - - - - - - - - - - - - - - - - -
2 | // - - modules
3 | // add new modules here
4 |
5 | // @import "example-module";
6 |
7 | @import forms
8 | @import modals
9 |
10 | @import main_header
11 | @import sessions
12 | @import boards
13 | @import members_selector
14 |
--------------------------------------------------------------------------------
/web/static/css/modules/_sessions.sass:
--------------------------------------------------------------------------------
1 | .sessions.new, .registrations.new
2 | background: $purple
3 | background-size: cover
4 | position: relative
5 | height: 100%
6 |
7 | main
8 | +transform(translate(-50%, -50%))
9 | position: absolute
10 | width: 450px
11 | top: 40%
12 | left: 50%
13 | text-align: center
14 |
15 | header
16 | text-align: center
17 | margin-bottom: 2em
18 |
19 | .logo
20 | display: inline-block
21 | background: url(/images/logo.png) 0 0 no-repeat
22 | background-size: contain
23 | height: 62px
24 | width: 198px
25 | vertical-align: middle
26 |
27 | form
28 | margin-bottom: $base-spacing
29 | .field
30 | margin: .5em 0
31 |
32 | [type="text"], [type="email"], [type="password"], button
33 | border: 0px none
34 | width: 350px
35 | padding: 1em
36 | font-size: 1.1em
37 |
38 | button
39 | +transition
40 | background: transparent
41 | color: #fff
42 | border: 1px solid #fff
43 |
44 | &:hover
45 | color: $purple
46 | background: #fff
47 |
48 | a
49 | +transition
50 | color: rgba(#fff, .5)
51 |
52 | &:hover
53 | color: #fff
54 |
--------------------------------------------------------------------------------
/web/static/js/actions/boards.js:
--------------------------------------------------------------------------------
1 | import Constants from '../constants';
2 | import { push } from 'react-router-redux';
3 | import { httpGet, httpPost } from '../utils';
4 | import CurrentBoardActions from './current_board';
5 |
6 | const Actions = {
7 | fetchBoards: () => {
8 | return dispatch => {
9 | dispatch({ type: Constants.BOARDS_FETCHING });
10 |
11 | httpGet('/api/v1/boards')
12 | .then((data) => {
13 | dispatch({
14 | type: Constants.BOARDS_RECEIVED,
15 | ownedBoards: data.owned_boards,
16 | invitedBoards: data.invited_boards,
17 | });
18 | });
19 | };
20 | },
21 |
22 | showForm: (show) => {
23 | return dispatch => {
24 | dispatch({
25 | type: Constants.BOARDS_SHOW_FORM,
26 | show: show,
27 | });
28 | };
29 | },
30 |
31 | create: (data) => {
32 | return dispatch => {
33 | httpPost('/api/v1/boards', { board: data })
34 | .then((data) => {
35 | dispatch({
36 | type: Constants.BOARDS_NEW_BOARD_CREATED,
37 | board: data,
38 | });
39 |
40 | dispatch(push(`/boards/${data.id}`));
41 | })
42 | .catch((error) => {
43 | error.response.json()
44 | .then((json) => {
45 | dispatch({
46 | type: Constants.BOARDS_CREATE_ERROR,
47 | errors: json.errors,
48 | });
49 | });
50 | });
51 | };
52 | },
53 |
54 | reset: () => {
55 | return dispatch => {
56 | dispatch({
57 | type: Constants.BOARDS_RESET,
58 | });
59 | };
60 | },
61 | };
62 |
63 | export default Actions;
64 |
--------------------------------------------------------------------------------
/web/static/js/actions/current_board.js:
--------------------------------------------------------------------------------
1 | import Constants from '../constants';
2 |
3 | const Actions = {
4 | showForm: (show) => {
5 | return dispatch => {
6 | dispatch({
7 | type: Constants.CURRENT_BOARD_SHOW_FORM,
8 | show: show,
9 | });
10 | };
11 | },
12 |
13 | connectToChannel: (socket, boardId) => {
14 | return dispatch => {
15 | const channel = socket.channel(`boards:${boardId}`);
16 |
17 | dispatch({ type: Constants.CURRENT_BOARD_FETCHING });
18 |
19 | channel.join().receive('ok', (response) => {
20 | dispatch({
21 | type: Constants.BOARDS_SET_CURRENT_BOARD,
22 | board: response.board,
23 | });
24 | });
25 |
26 | channel.on('user:joined', (msg) => {
27 | dispatch({
28 | type: Constants.CURRENT_BOARD_CONNECTED_USERS,
29 | users: msg.users,
30 | });
31 | });
32 |
33 | channel.on('user:left', (msg) => {
34 | dispatch({
35 | type: Constants.CURRENT_BOARD_CONNECTED_USERS,
36 | users: msg.users,
37 | });
38 | });
39 |
40 | channel.on('list:created', (msg) => {
41 | dispatch({
42 | type: Constants.CURRENT_BOARD_LIST_CREATED,
43 | list: msg.list,
44 | });
45 | });
46 |
47 | channel.on('card:created', (msg) => {
48 | dispatch({
49 | type: Constants.CURRENT_BOARD_CARD_CREATED,
50 | card: msg.card,
51 | });
52 | });
53 |
54 | channel.on('member:added', (msg) => {
55 | dispatch({
56 | type: Constants.CURRENT_BOARD_MEMBER_ADDED,
57 | user: msg.user,
58 | });
59 | });
60 |
61 | channel.on('card:updated', (msg) => {
62 | dispatch({
63 | type: Constants.BOARDS_SET_CURRENT_BOARD,
64 | board: msg.board,
65 | });
66 |
67 | dispatch({
68 | type: Constants.CURRENT_CARD_SET,
69 | card: msg.card,
70 | });
71 | });
72 |
73 | channel.on('list:updated', (msg) => {
74 | dispatch({
75 | type: Constants.BOARDS_SET_CURRENT_BOARD,
76 | board: msg.board,
77 | });
78 | });
79 |
80 | channel.on('comment:created', (msg) => {
81 | dispatch({
82 | type: Constants.BOARDS_SET_CURRENT_BOARD,
83 | board: msg.board,
84 | });
85 |
86 | dispatch({
87 | type: Constants.CURRENT_CARD_SET,
88 | card: msg.card,
89 | });
90 | });
91 |
92 | dispatch({
93 | type: Constants.CURRENT_BOARD_CONNECTED_TO_CHANNEL,
94 | channel: channel,
95 | });
96 | };
97 | },
98 |
99 | leaveChannel: (channel) => {
100 | return dispatch => {
101 | channel.leave();
102 |
103 | dispatch({
104 | type: Constants.CURRENT_BOARD_RESET,
105 | });
106 | };
107 | },
108 |
109 | addNewMember: (channel, email) => {
110 | return dispatch => {
111 | channel.push('members:add', { email: email })
112 | .receive('error', (data) => {
113 | dispatch({
114 | type: Constants.CURRENT_BOARD_ADD_MEMBER_ERROR,
115 | error: data.error,
116 | });
117 | });
118 | };
119 | },
120 |
121 | updateCard: (channel, card) => {
122 | return dispatch => {
123 | channel.push('card:update', { card: card });
124 | };
125 | },
126 |
127 | updateList: (channel, list) => {
128 | return dispatch => {
129 | channel.push('list:update', { list: list });
130 | };
131 | },
132 |
133 | showMembersForm: (show) => {
134 | return dispatch => {
135 | dispatch({
136 | type: Constants.CURRENT_BOARD_SHOW_MEMBERS_FORM,
137 | show: show,
138 | });
139 | };
140 | },
141 |
142 | editList: (listId) => {
143 | return dispatch => {
144 | dispatch({
145 | type: Constants.CURRENT_BOARD_EDIT_LIST,
146 | listId: listId,
147 | });
148 | };
149 | },
150 |
151 | showCardForm: (listId) => {
152 | return dispatch => {
153 | dispatch({
154 | type: Constants.CURRENT_BOARD_SHOW_CARD_FORM_FOR_LIST,
155 | listId: listId,
156 | });
157 | };
158 | },
159 | };
160 |
161 | export default Actions;
162 |
--------------------------------------------------------------------------------
/web/static/js/actions/current_card.js:
--------------------------------------------------------------------------------
1 | import Constants from '../constants';
2 | import {httpGet} from '../utils';
3 |
4 | const Actions = {
5 | showCard: (card) => {
6 | return dispatch => {
7 | dispatch({
8 | type: Constants.CURRENT_CARD_SET,
9 | card: card
10 | });
11 | };
12 | },
13 |
14 | editCard: (edit) => {
15 | return dispatch => {
16 | dispatch({
17 | type: Constants.CURRENT_CARD_EDIT,
18 | edit: edit,
19 | });
20 | };
21 | },
22 |
23 | createCardComment: (channel, comment) => {
24 | return dispatch => {
25 | channel.push('card:add_comment', comment);
26 | };
27 | },
28 |
29 | reset: (channel, comment) => {
30 | return dispatch => {
31 | dispatch({
32 | type: Constants.CURRENT_CARD_RESET,
33 | });
34 | };
35 | },
36 |
37 | showMembersSelector: (show) => {
38 | return dispatch => {
39 | dispatch({
40 | type: Constants.CURRENT_CARD_SHOW_MEMBERS_SELECTOR,
41 | show: show,
42 | });
43 | };
44 | },
45 |
46 | showTagsSelector: (show) => {
47 | return dispatch => {
48 | dispatch({
49 | type: Constants.CURRENT_CARD_SHOW_TAGS_SELECTOR,
50 | show: show,
51 | });
52 | };
53 | },
54 |
55 | addMember: (channel, cardId, userId) => {
56 | return dispatch => {
57 | channel.push('card:add_member', { card_id: cardId, user_id: userId });
58 | };
59 | },
60 |
61 | removeMember: (channel, cardId, userId) => {
62 | return dispatch => {
63 | channel.push('card:remove_member', { card_id: cardId, user_id: userId });
64 | };
65 | },
66 |
67 | updateTags: (channel, cardId, tags) => {
68 | return dispatch => {
69 | channel.push('card:update', { card: { id: cardId, tags: tags } });
70 | };
71 | },
72 | };
73 |
74 | export default Actions;
75 |
--------------------------------------------------------------------------------
/web/static/js/actions/header.js:
--------------------------------------------------------------------------------
1 | import Constants from '../constants';
2 | import { push } from 'react-router-redux';
3 | import { httpGet, httpPost } from '../utils';
4 | import CurrentBoardActions from './current_board';
5 |
6 | const Actions = {
7 | showBoards: (show) => {
8 | return dispatch => {
9 | dispatch({
10 | type: Constants.HEADER_SHOW_BOARDS,
11 | show: show,
12 | });
13 | };
14 | },
15 |
16 | visitBoard: (socket, channel, boardId) => {
17 | return dispatch => {
18 | if (channel) {
19 | dispatch(CurrentBoardActions.leaveChannel(channel));
20 | dispatch(CurrentBoardActions.connectToChannel(socket, boardId));
21 | }
22 |
23 | dispatch(push(`/boards/${boardId}`));
24 |
25 | dispatch({
26 | type: Constants.HEADER_SHOW_BOARDS,
27 | show: false,
28 | });
29 | };
30 | },
31 | };
32 |
33 | export default Actions;
34 |
--------------------------------------------------------------------------------
/web/static/js/actions/lists.js:
--------------------------------------------------------------------------------
1 | import Constants from '../constants';
2 |
3 | const Actions = {
4 | showForm: (show) => {
5 | return dispatch => {
6 | dispatch({
7 | type: Constants.LISTS_SHOW_FORM,
8 | show: show,
9 | });
10 | };
11 | },
12 |
13 | save: (channel, data) => {
14 | return dispatch => {
15 | const topic = data.id ? 'list:update' : 'lists:create';
16 |
17 | channel.push(topic, { list: data });
18 | };
19 | },
20 |
21 | createCard: (channel, data) => {
22 | return dispatch => {
23 | channel.push('cards:create', { card: data });
24 | };
25 | },
26 | };
27 |
28 | export default Actions;
29 |
--------------------------------------------------------------------------------
/web/static/js/actions/registrations.js:
--------------------------------------------------------------------------------
1 | import { push } from 'react-router-redux';
2 | import Constants from '../constants';
3 | import { httpPost } from '../utils';
4 | import {setCurrentUser} from './sessions';
5 |
6 | const Actions = {};
7 |
8 | Actions.signUp = (data) => {
9 | return dispatch => {
10 | httpPost('/api/v1/registrations', { user: data })
11 | .then((data) => {
12 | localStorage.setItem('phoenixAuthToken', data.jwt);
13 |
14 | setCurrentUser(dispatch, data.user);
15 |
16 | dispatch(push('/'));
17 | })
18 | .catch((error) => {
19 | error.response.json()
20 | .then((errorJSON) => {
21 | dispatch({
22 | type: Constants.REGISTRATIONS_ERROR,
23 | errors: errorJSON.errors,
24 | });
25 | });
26 | });
27 | };
28 | };
29 |
30 | export default Actions;
31 |
--------------------------------------------------------------------------------
/web/static/js/actions/sessions.js:
--------------------------------------------------------------------------------
1 | import { push } from 'react-router-redux';
2 | import Constants from '../constants';
3 | import { Socket } from 'phoenix';
4 | import { httpGet, httpPost, httpDelete } from '../utils';
5 |
6 | export function setCurrentUser(dispatch, user) {
7 | const socket = new Socket('/socket', {
8 | params: { token: localStorage.getItem('phoenixAuthToken') },
9 | logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data); },
10 | });
11 |
12 | socket.connect();
13 |
14 | const channel = socket.channel(`users:${user.id}`);
15 |
16 | if (channel.state != 'joined') {
17 | channel.join().receive('ok', () => {
18 | dispatch({
19 | type: Constants.CURRENT_USER,
20 | currentUser: user,
21 | socket: socket,
22 | channel: channel,
23 | });
24 | });
25 | }
26 |
27 | channel.on('boards:add', (msg) => {
28 | dispatch({
29 | type: Constants.BOARDS_ADDED,
30 | board: msg.board,
31 | });
32 | });
33 | };
34 |
35 | const Actions = {
36 | signIn: (email, password) => {
37 | return dispatch => {
38 | const data = {
39 | session: {
40 | email: email,
41 | password: password,
42 | },
43 | };
44 |
45 | httpPost('/api/v1/sessions', data)
46 | .then((data) => {
47 | localStorage.setItem('phoenixAuthToken', data.jwt);
48 | setCurrentUser(dispatch, data.user);
49 | dispatch(push('/'));
50 | })
51 | .catch((error) => {
52 | error.response.json()
53 | .then((errorJSON) => {
54 | dispatch({
55 | type: Constants.SESSIONS_ERROR,
56 | error: errorJSON.error,
57 | });
58 | });
59 | });
60 | };
61 | },
62 |
63 | currentUser: () => {
64 | return dispatch => {
65 | const authToken = localStorage.getItem('phoenixAuthToken');
66 |
67 | httpGet('/api/v1/current_user')
68 | .then(function (data) {
69 | setCurrentUser(dispatch, data);
70 | })
71 | .catch(function (error) {
72 | console.log(error);
73 | dispatch(push('/sign_in'));
74 | });
75 | };
76 | },
77 |
78 | signOut: () => {
79 | return dispatch => {
80 | httpDelete('/api/v1/sessions')
81 | .then((data) => {
82 | localStorage.removeItem('phoenixAuthToken');
83 |
84 | dispatch({ type: Constants.USER_SIGNED_OUT, });
85 |
86 | dispatch(push('/sign_in'));
87 |
88 | dispatch({ type: Constants.BOARDS_FULL_RESET });
89 | })
90 | .catch(function (error) {
91 | console.log(error);
92 | });
93 | };
94 | },
95 | };
96 |
97 | export default Actions;
98 |
--------------------------------------------------------------------------------
/web/static/js/application.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { browserHistory } from 'react-router';
4 | import { syncHistoryWithStore } from 'react-router-redux';
5 | import configureStore from './store';
6 | import Root from './containers/root';
7 |
8 | const store = configureStore(browserHistory);
9 | const history = syncHistoryWithStore(browserHistory, store);
10 |
11 | const target = document.getElementById('main_container');
12 | const node =
{card.description}
160 | Edit 161 |