├── .gitignore ├── Capfile ├── LICENSE ├── README.md ├── brunch-config.js ├── config ├── config.exs ├── deploy.rb ├── deploy │ └── production.rb ├── dev.exs ├── prod.exs └── test.exs ├── lib ├── dash.ex └── dash │ ├── endpoint.ex │ └── repo.ex ├── mix.exs ├── mix.lock ├── package.json ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── default.po │ │ │ └── errors.po │ └── errors.pot └── repo │ ├── migrations │ ├── 20150904214950_create_post.exs │ ├── 20150904220940_add_published_to_posts.exs │ ├── 20150904221251_add_permalink_unique_index_to_posts.exs │ ├── 20150907212409_add_published_at_and_summary_to_posts.exs │ ├── 20150917184838_add_users_table.exs │ └── 20150917191613_add_user_to_posts.exs │ └── seeds.exs ├── rel ├── config.exs └── plugins │ └── phoenix_digest_task.exs ├── test ├── controllers │ ├── page_controller_test.exs │ └── post_controller_test.exs ├── models │ └── post_test.exs ├── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── model_case.ex ├── test_helper.exs └── views │ ├── error_view_test.exs │ ├── layout_view_test.exs │ └── page_view_test.exs └── web ├── channels └── user_socket.ex ├── controllers ├── atom_controller.ex ├── page_controller.ex ├── post_controller.ex └── sitemap_controller.ex ├── gettext.ex ├── models ├── post.ex └── user.ex ├── router.ex ├── static ├── assets │ ├── favicon.ico │ ├── fonts │ │ ├── 2FD706_0_0.eot │ │ ├── 2FD706_0_0.ttf │ │ ├── 2FD706_0_0.woff │ │ └── 2FD706_0_0.woff2 │ ├── images │ │ ├── ic-mail.svg │ │ ├── main-logo.png │ │ ├── mario_chavez.jpg │ │ ├── phoenix.png │ │ └── sprites.svg │ └── robots.txt ├── css │ ├── _font.scss │ ├── _mixings.scss │ ├── _variables.scss │ ├── admin.scss │ └── app.scss ├── js │ ├── app.js │ ├── menu.coffee │ └── socket.js └── vendor │ ├── css │ ├── grids-responsive-min.css │ └── pure-min.css │ └── js │ └── jquery-2.1.4.min.js ├── templates ├── atom │ └── index.html.eex ├── layout │ ├── app.html.eex │ └── meta.html.eex ├── page │ ├── author.html.eex │ ├── index.html.eex │ ├── menu.html.eex │ └── show.html.eex ├── post │ ├── edit.html.eex │ ├── form.html.eex │ ├── index.html.eex │ ├── menu.html.eex │ ├── new.html.eex │ └── show.html.eex └── sitemap │ └── index.html.eex ├── views ├── atom_view.ex ├── error_helpers.ex ├── error_view.ex ├── layout_view.ex ├── page_view.ex ├── post_helper.ex ├── post_view.ex └── sitemap_view.ex └── web.ex /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # Static artifacts 11 | /node_modules 12 | 13 | # Since we are building assets from web/static, 14 | # we ignore priv/static. You may want to comment 15 | # this depending on your deployment strategy. 16 | /priv/static/ 17 | 18 | # The config/prod.secret.exs file by default contains sensitive 19 | # data and you should not commit it into version control. 20 | # 21 | # Alternatively, you may comment the line below and commit the 22 | # secrets file as long as you replace its contents by environment 23 | # variables. 24 | /config/prod.secret.exs 25 | 26 | *.swp 27 | *.~un 28 | .DS_Store 29 | npm-debug* 30 | *.swp 31 | *.un~ 32 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | # Load DSL and set up stages 2 | require 'capistrano/setup' 3 | 4 | # Include default deployment tasks 5 | require 'capistrano/deploy' 6 | 7 | # Include tasks from other gems included in your Gemfile 8 | # 9 | # For documentation on these, see for example: 10 | # 11 | # https://github.com/capistrano/rvm 12 | # https://github.com/capistrano/rbenv 13 | # https://github.com/capistrano/chruby 14 | # https://github.com/capistrano/bundler 15 | # https://github.com/capistrano/rails 16 | # https://github.com/capistrano/passenger 17 | # 18 | # require 'capistrano/rvm' 19 | # require 'capistrano/rbenv' 20 | # require 'capistrano/chruby' 21 | # require 'capistrano/bundler' 22 | # require 'capistrano/rails/assets' 23 | # require 'capistrano/rails/migrations' 24 | # require 'capistrano/passenger' 25 | 26 | # Load custom tasks from `lib/capistrano/tasks` if you have any defined 27 | Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 michelada.io 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dash 2 | 3 | To start your Phoenix app: 4 | 5 | 1. Install dependencies with `mix deps.get` 6 | 2. Create and migrate your database with `mix ecto.create && mix ecto.migrate` 7 | 3. Export `ADMIN_PATH` env variable, be sure this is unique and secure, since 8 | this will take you to the admin area of the blog 9 | 4. Start Phoenix endpoint with `mix phoenix.server` 10 | 11 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 12 | To get to the admin area, visit [`localhost:4000/ADMIN_PATH/posts`](http://localhost:4000/ADMIN_PATH/posts) from your browser where `ADMIN_POST` is what you set in your env variable. 13 | 14 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment). 15 | 16 | ## License 17 | Copyright 2017 michelada.io 18 | See LICENCE 19 | -------------------------------------------------------------------------------- /brunch-config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // See http://brunch.io/#documentation for docs. 3 | files: { 4 | javascripts: { 5 | joinTo: 'js/app.js' 6 | 7 | // To use a separate vendor.js bundle, specify two files path 8 | // https://github.com/brunch/brunch/blob/stable/docs/config.md#files 9 | // joinTo: { 10 | // 'js/app.js': /^(web\/static\/js)/, 11 | // 'js/vendor.js': /^(web\/static\/vendor)|(deps)/ 12 | // } 13 | // 14 | // To change the order of concatenation of files, explicitly mention here 15 | // https://github.com/brunch/brunch/tree/master/docs#concatenation 16 | // order: { 17 | // before: [ 18 | // 'web/static/vendor/js/jquery-2.1.1.js', 19 | // 'web/static/vendor/js/bootstrap.min.js' 20 | // ] 21 | // } 22 | }, 23 | stylesheets: { 24 | order: { 25 | before: [ 26 | 'web/static/vendor/css/pure-min.css', 27 | 'web/static/vendor/css/grids-responsive-min.css' 28 | ] 29 | }, 30 | joinTo: { 31 | 'css/app.css': /^(web\/static\/css\/app)|(web\/static\/vendor\/css)/, 32 | 'css/admin.css': /^web\/static\/css\/admin/ 33 | } 34 | }, 35 | templates: { 36 | joinTo: 'js/app.js' 37 | } 38 | }, 39 | 40 | conventions: { 41 | // This option sets where we should place non-css and non-js assets in. 42 | // By default, we set this to '/web/static/assets'. Files in this directory 43 | // will be copied to `paths.public`, which is "priv/static" by default. 44 | assets: /^(web\/static\/assets)/ 45 | }, 46 | 47 | // Phoenix paths configuration 48 | paths: { 49 | // Dependencies and current project directories to watch 50 | watched: ["deps/phoenix/web/static", 51 | "deps/phoenix_html/web/static", 52 | "web/static", "test/static"], 53 | 54 | // Where to compile files to 55 | public: "priv/static" 56 | }, 57 | 58 | // Configure your plugins 59 | plugins: { 60 | babel: { 61 | // Do not use ES6 compiler in vendor code 62 | ignore: [/web\/static\/vendor/] 63 | } 64 | }, 65 | 66 | modules: { 67 | autoRequire: { 68 | 'js/app.js': ['web/static/js/app'] 69 | } 70 | }, 71 | 72 | npm: { 73 | enabled: true 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # General application configuration 9 | config :dash, 10 | ecto_repos: [Dash.Repo] 11 | 12 | # Configures the endpoint 13 | config :dash, Dash.Endpoint, 14 | url: [host: "localhost"], 15 | secret_key_base: "vdg039YkEHe0Hsj6pwY2rlTGYDUh2d5TcLftkgVzfJBL3GKLD5FfXQ2vJD8EuweX", 16 | render_errors: [view: Dash.ErrorView, accepts: ~w(html json)], 17 | pubsub: [name: Dash.PubSub, 18 | adapter: Phoenix.PubSub.PG2] 19 | 20 | # Configures Elixir's Logger 21 | config :logger, :console, 22 | format: "$time $metadata[$level] $message\n", 23 | metadata: [:request_id] 24 | 25 | # Import environment specific config. This must remain at the bottom 26 | # of this file so it overrides the configuration defined above. 27 | import_config "#{Mix.env}.exs" 28 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | # config valid only for current version of Capistrano 2 | lock '3.8.0' 3 | 4 | set :application , 'michelada.io-blog' 5 | set :repo_url , 'git@github.com:michelada/dash.git' 6 | ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp 7 | set :default_env , { mix_env: "prod", HOST: "blog.michelada.io", PUBLIC_PORT: 443, SCHEME: "https", PORT: 4002, ADMIN_PATH: ENV["ADMIN_PATH"], DB_URL: ENV["DB_URL"] } 8 | set :deploy_to , '/home/deploy/www/dash-blog' 9 | set :log_level , :debug 10 | set :keep_releases , 2 11 | 12 | set :linked_files, fetch(:linked_files, []).push('config/prod.secret.exs') 13 | set :linked_dirs, fetch(:linked_dirs, []). 14 | push('deps', 'node_modules', '_build', 'priv/static/css', 'priv/static/js') 15 | 16 | 17 | task :dependencies do 18 | on roles(:web) do |host| 19 | within(current_path) do 20 | execute(:mix, "local.hex", "--force") 21 | execute(:mix, "local.rebar", "--force") 22 | execute(:mix, "deps.get") 23 | end 24 | end 25 | end 26 | 27 | # Run manually as needed 28 | namespace :node do 29 | task :dependencies do 30 | on roles(:web) do |host| 31 | within(current_path) do 32 | execute(:npm, "install") 33 | end 34 | end 35 | end 36 | end 37 | 38 | task :build do 39 | on roles(:web) do |host| 40 | within(current_path) do 41 | execute(:mix, "release --env=prod") 42 | end 43 | end 44 | end 45 | 46 | task :clean do 47 | on roles(:web) do |host| 48 | within(current_path) do 49 | execute(:mix, "deps.clean", "--all") 50 | end 51 | end 52 | end 53 | 54 | task :assets do 55 | on roles(:web) do |host| 56 | within(current_path) do 57 | execute(:brunch, "build", "--production") 58 | execute(:mix, "phoenix.digest") 59 | end 60 | end 61 | end 62 | 63 | task :migrations do 64 | on roles(:web) do |host| 65 | within(current_path) do 66 | execute(:mix, "ecto.migrate") 67 | end 68 | end 69 | end 70 | 71 | task :start do 72 | on roles(:web) do |host| 73 | within("#{current_path}/_build/prod/rel/dash/bin") do 74 | execute("./dash", "start") 75 | end 76 | end 77 | end 78 | 79 | task :stop do 80 | on roles(:web) do |host| 81 | within("#{current_path}/_build/prod/rel/dash/bin") do 82 | execute("./dash", "stop") 83 | end 84 | end 85 | end 86 | 87 | task :restart do 88 | on roles(:web) do |host| 89 | invoke("stop") 90 | invoke("start") 91 | end 92 | end 93 | 94 | before :build, :dependencies 95 | before :build, "node:dependencies" 96 | before :build, :assets 97 | after "deploy:published", :build 98 | #after "deploy:published", :restart 99 | -------------------------------------------------------------------------------- /config/deploy/production.rb: -------------------------------------------------------------------------------- 1 | server 'dp', user: 'deploy', roles: %w{web} 2 | -------------------------------------------------------------------------------- /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 :dash, Dash.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin", 15 | cd: Path.expand("../", __DIR__)]] 16 | 17 | 18 | # Watch static and templates for browser reloading. 19 | config :dash, Dash.Endpoint, 20 | live_reload: [ 21 | patterns: [ 22 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 23 | ~r{priv/gettext/.*(po)$}, 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. Avoid configuring such 33 | # in production as building large stacktraces may be expensive. 34 | config :phoenix, :stacktrace_depth, 20 35 | 36 | # Configure your database 37 | config :dash, Dash.Repo, 38 | adapter: Ecto.Adapters.Postgres, 39 | database: "dash_dev", 40 | hostname: "localhost", 41 | pool_size: 10 42 | 43 | config :dash, 44 | admin_path: System.get_env("ADMIN_PATH") || 'admin' 45 | -------------------------------------------------------------------------------- /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 :dash, Dash.Endpoint, 15 | http: [port: System.get_env("PORT")], 16 | url: [scheme: System.get_env("SCHEME"), host: System.get_env("HOST"), port: System.get_env("PUBLIC_PORT")], 17 | static_url: [scheme: System.get_env("SCHEME"), host: System.get_env("HOST"), port: System.get_env("PUBLIC_PORT")], 18 | cache_static_manifest: "priv/static/manifest.json", 19 | server: true, 20 | root: ".", 21 | version: Mix.Project.config[:version] 22 | 23 | # Do not print debug messages in production 24 | config :logger, level: :info 25 | 26 | # ## SSL Support 27 | # 28 | # To get SSL working, you will need to add the `https` key 29 | # to the previous section and set your `:url` port to 443: 30 | # 31 | # config :dash, Dash.Endpoint, 32 | # ... 33 | # url: [host: "example.com", port: 443], 34 | # https: [port: 443, 35 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 36 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 37 | # 38 | # Where those two env variables return an absolute path to 39 | # the key and cert in disk or a relative path inside priv, 40 | # for example "priv/ssl/server.key". 41 | # 42 | # We also recommend setting `force_ssl`, ensuring no data is 43 | # ever sent via http, always redirecting to https: 44 | # 45 | # config :dash, Dash.Endpoint, 46 | # force_ssl: [hsts: true] 47 | # 48 | # Check `Plug.SSL` for all available options in `force_ssl`. 49 | 50 | # ## Using releases 51 | # 52 | # If you are doing OTP releases, you need to instruct Phoenix 53 | # to start the server for all endpoints: 54 | # 55 | # config :phoenix, :serve_endpoints, true 56 | # 57 | # Alternatively, you can configure exactly which server to 58 | # start per endpoint: 59 | # 60 | # config :dash, Dash.Endpoint, server: true 61 | # 62 | 63 | # Finally import the config/prod.secret.exs 64 | # which should be versioned separately. 65 | config :phoenix, :serve_endpoints, true 66 | 67 | config :dash, Dash.Repo, 68 | adapter: Ecto.Adapters.Postgres, 69 | url: System.get_env("DB_URL"), 70 | pool_size: 10 71 | 72 | config :dash, 73 | admin_path: System.get_env("ADMIN_PATH") || 'admin' 74 | 75 | import_config "prod.secret.exs" 76 | -------------------------------------------------------------------------------- /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 :dash, Dash.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 :dash, Dash.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | database: "dash_test", 16 | hostname: "localhost", 17 | pool: Ecto.Adapters.SQL.Sandbox 18 | -------------------------------------------------------------------------------- /lib/dash.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash 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 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | # Start the Ecto repository 12 | supervisor(Dash.Repo, []), 13 | # Start the endpoint when the application starts 14 | supervisor(Dash.Endpoint, []), 15 | # Start your own worker by calling: Dash.Worker.start_link(arg1, arg2, arg3) 16 | # worker(Dash.Worker, [arg1, arg2, arg3]), 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: Dash.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 | Dash.Endpoint.config_change(changed, removed) 29 | :ok 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/dash/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :dash 3 | 4 | socket "/socket", Dash.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: :dash, 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 | # The session will be stored in the cookie and signed, 34 | # this means its contents can be read but not tampered with. 35 | # Set :encryption_salt if you would also like to encrypt it. 36 | plug Plug.Session, 37 | store: :cookie, 38 | key: "_dash_key", 39 | signing_salt: "xyejedK5" 40 | 41 | plug Dash.Router 42 | end 43 | -------------------------------------------------------------------------------- /lib/dash/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.Repo do 2 | use Ecto.Repo, otp_app: :dash 3 | end 4 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Dash.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :dash, 6 | version: "0.1.0", 7 | elixir: "~> 1.2", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | aliases: aliases(), 13 | deps: deps()] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [mod: {Dash, []}, 21 | applications: [:gettext, :phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, 22 | :phoenix_ecto, :postgrex, :earmark, :chronos]] 23 | end 24 | 25 | # Specifies which paths to compile per environment. 26 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"] 27 | defp elixirc_paths(_), do: ["lib", "web"] 28 | 29 | # Specifies your project dependencies. 30 | # 31 | # Type `mix help deps` for examples and options. 32 | defp deps do 33 | [{:phoenix, "~> 1.2.3"}, 34 | {:phoenix_pubsub, "~> 1.0"}, 35 | {:phoenix_ecto, "~> 3.0"}, 36 | {:postgrex, ">= 0.0.0"}, 37 | {:phoenix_html, "~> 2.6"}, 38 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 39 | {:gettext, "~> 0.13.1"}, 40 | {:earmark, "~>1.1"}, 41 | {:distillery, "~> 1.0"}, 42 | {:chronos, "~> 1.7.0"}, 43 | {:cowboy, "~> 1.0"}] 44 | end 45 | 46 | # Aliases are shortcuts or tasks specific to the current project. 47 | # For example, to create, migrate and run the seeds file at once: 48 | # 49 | # $ mix ecto.setup 50 | # 51 | # See the documentation for `Mix` for more info on aliases. 52 | defp aliases do 53 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 54 | "ecto.reset": ["ecto.drop", "ecto.setup"], 55 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bbmustache": {:hex, :bbmustache, "1.0.4", "7ba94f971c5afd7b6617918a4bb74705e36cab36eb84b19b6a1b7ee06427aa38", [:rebar], []}, 2 | "cf": {:hex, :cf, "0.2.2", "7f2913fff90abcabd0f489896cfeb0b0674f6c8df6c10b17a83175448029896c", [:rebar3], []}, 3 | "chronos": {:hex, :chronos, "1.7.0", "aee907f6bcc11a566cfbaf8b5850799e6cf7e8d651013fbbcd653b98a70c112d", [:mix], []}, 4 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []}, 5 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, optional: false]}]}, 6 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, 7 | "db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]}, 8 | "decimal": {:hex, :decimal, "1.3.1", "157b3cedb2bfcb5359372a7766dd7a41091ad34578296e951f58a946fcab49c6", [:mix], []}, 9 | "distillery": {:hex, :distillery, "1.2.2", "d5a52920cbe2378c8a21dfc83b526b4225944b9dce7bf170fe5f5cddda81ffb3", [:mix], []}, 10 | "earmark": {:hex, :earmark, "1.2.0", "bf1ce17aea43ab62f6943b97bd6e3dc032ce45d4f787504e3adf738e54b42f3a", [:mix], []}, 11 | "ecto": {:hex, :ecto, "2.1.4", "d1ba932813ec0e0d9db481ef2c17777f1cefb11fc90fa7c142ff354972dfba7e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]}, 12 | "erlware_commons": {:hex, :erlware_commons, "0.22.0", "051bed79a34e66678c1cbeebc7b66360c827d081a0c5e2528878011e31ddcdca", [:rebar3], [{:cf, "0.2.2", [hex: :cf, optional: false]}]}, 13 | "exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]}, 14 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []}, 15 | "getopt": {:hex, :getopt, "0.8.2", "b17556db683000ba50370b16c0619df1337e7af7ecbf7d64fbf8d1d6bce3109b", [:rebar], []}, 16 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []}, 17 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], []}, 18 | "phoenix": {:hex, :phoenix, "1.2.3", "b68dd6a7e6ff3eef38ad59771007d2f3f344988ea6e658e9b2c6ffb2ef494810", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.4 or ~> 1.3.3 or ~> 1.2.4 or ~> 1.1.8 or ~> 1.0.5", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]}, 19 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.2.3", "450c749876ff1de4a78fdb305a142a76817c77a1cd79aeca29e5fc9a6c630b26", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]}, 20 | "phoenix_html": {:hex, :phoenix_html, "2.9.3", "1b5a2122cbf743aa242f54dced8a4f1cc778b8bd304f4b4c0043a6250c58e258", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]}, 21 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.8", "4333f9c74190f485a74866beff2f9304f069d53f047f5fbb0fb8d1ee4c495f73", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, optional: false]}]}, 22 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.1", "c10ddf6237007c804bf2b8f3c4d5b99009b42eca3a0dfac04ea2d8001186056a", [:mix], []}, 23 | "plug": {:hex, :plug, "1.3.4", "b4ef3a383f991bfa594552ded44934f2a9853407899d47ecc0481777fb1906f6", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, 24 | "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}, 25 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}, 26 | "postgrex": {:hex, :postgrex, "0.13.2", "2b88168fc6a5456a27bfb54ccf0ba4025d274841a7a3af5e5deb1b755d95154e", [:mix], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]}, 27 | "providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]}, 28 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], []}, 29 | "relx": {:hex, :relx, "3.22.2", "aee2ef6e9ac6d21d6661133b7a0be6e81424de9cdca0012fc008bc677297c469", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.2", [hex: :cf, optional: false]}, {:erlware_commons, "0.22.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]}} 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "license": "MIT", 4 | "scripts": { 5 | "deploy": "brunch build --production", 6 | "watch": "brunch watch --stdin" 7 | }, 8 | "dependencies": { 9 | "phoenix": "file:deps/phoenix", 10 | "phoenix_html": "file:deps/phoenix_html" 11 | }, 12 | "devDependencies": { 13 | "babel-brunch": "~6.0.0", 14 | "brunch": "2.7.4", 15 | "clean-css-brunch": "~2.0.0", 16 | "css-brunch": "~2.0.0", 17 | "javascript-brunch": "~2.0.0", 18 | "uglify-js-brunch": "~2.0.1", 19 | "sass-brunch": "^1.8.10", 20 | "coffee-script-brunch": ">= 1.8" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/default.po: -------------------------------------------------------------------------------- 1 | msgid "We plan, code and launch awesome web and mobile products." 2 | msgstr "" 3 | 4 | msgid "We work with startups and established companies implementing great ideas with simplicity and code quality" 5 | msgstr "" 6 | 7 | msgid "design, develop, startup, code, ruby, rails, node, javascript, elixir, angularjs, reactjs, ios, android, app, legacy, maintenance, upgrade" 8 | msgstr "" 9 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_format/3 26 | msgid "has invalid format" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_subset/3 30 | msgid "has an invalid entry" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_exclusion/3 34 | msgid "is reserved" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_confirmation/3 38 | msgid "does not match confirmation" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.no_assoc_constraint/3 42 | msgid "is still associated to this entry" 43 | msgstr "" 44 | 45 | msgid "are still associated to this entry" 46 | msgstr "" 47 | 48 | ## From Ecto.Changeset.validate_length/3 49 | msgid "should be %{count} character(s)" 50 | msgid_plural "should be %{count} character(s)" 51 | msgstr[0] "" 52 | msgstr[1] "" 53 | 54 | msgid "should have %{count} item(s)" 55 | msgid_plural "should have %{count} item(s)" 56 | msgstr[0] "" 57 | msgstr[1] "" 58 | 59 | msgid "should be at least %{count} character(s)" 60 | msgid_plural "should be at least %{count} character(s)" 61 | msgstr[0] "" 62 | msgstr[1] "" 63 | 64 | msgid "should have at least %{count} item(s)" 65 | msgid_plural "should have at least %{count} item(s)" 66 | msgstr[0] "" 67 | msgstr[1] "" 68 | 69 | msgid "should be at most %{count} character(s)" 70 | msgid_plural "should be at most %{count} character(s)" 71 | msgstr[0] "" 72 | msgstr[1] "" 73 | 74 | msgid "should have at most %{count} item(s)" 75 | msgid_plural "should have at most %{count} item(s)" 76 | msgstr[0] "" 77 | msgstr[1] "" 78 | 79 | ## From Ecto.Changeset.validate_number/3 80 | msgid "must be less than %{number}" 81 | msgstr "" 82 | 83 | msgid "must be greater than %{number}" 84 | msgstr "" 85 | 86 | msgid "must be less than or equal to %{number}" 87 | msgstr "" 88 | 89 | msgid "must be greater than or equal to %{number}" 90 | msgstr "" 91 | 92 | msgid "must be equal to %{number}" 93 | msgstr "" 94 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_format/3 24 | msgid "has invalid format" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_subset/3 28 | msgid "has an invalid entry" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_exclusion/3 32 | msgid "is reserved" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_confirmation/3 36 | msgid "does not match confirmation" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.no_assoc_constraint/3 40 | msgid "is still associated to this entry" 41 | msgstr "" 42 | 43 | msgid "are still associated to this entry" 44 | msgstr "" 45 | 46 | ## From Ecto.Changeset.validate_length/3 47 | msgid "should be %{count} character(s)" 48 | msgid_plural "should be %{count} character(s)" 49 | msgstr[0] "" 50 | msgstr[1] "" 51 | 52 | msgid "should have %{count} item(s)" 53 | msgid_plural "should have %{count} item(s)" 54 | msgstr[0] "" 55 | msgstr[1] "" 56 | 57 | msgid "should be at least %{count} character(s)" 58 | msgid_plural "should be at least %{count} character(s)" 59 | msgstr[0] "" 60 | msgstr[1] "" 61 | 62 | msgid "should have at least %{count} item(s)" 63 | msgid_plural "should have at least %{count} item(s)" 64 | msgstr[0] "" 65 | msgstr[1] "" 66 | 67 | msgid "should be at most %{count} character(s)" 68 | msgid_plural "should be at most %{count} character(s)" 69 | msgstr[0] "" 70 | msgstr[1] "" 71 | 72 | msgid "should have at most %{count} item(s)" 73 | msgid_plural "should have at most %{count} item(s)" 74 | msgstr[0] "" 75 | msgstr[1] "" 76 | 77 | ## From Ecto.Changeset.validate_number/3 78 | msgid "must be less than %{number}" 79 | msgstr "" 80 | 81 | msgid "must be greater than %{number}" 82 | msgstr "" 83 | 84 | msgid "must be less than or equal to %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than or equal to %{number}" 88 | msgstr "" 89 | 90 | msgid "must be equal to %{number}" 91 | msgstr "" 92 | -------------------------------------------------------------------------------- /priv/repo/migrations/20150904214950_create_post.exs: -------------------------------------------------------------------------------- 1 | defmodule Dash.Repo.Migrations.CreatePost do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:posts) do 6 | add :title, :string 7 | add :body, :text 8 | add :permalink, :string 9 | add :tags, {:array, :string} 10 | 11 | timestamps() 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/migrations/20150904220940_add_published_to_posts.exs: -------------------------------------------------------------------------------- 1 | defmodule Dash.Repo.Migrations.AddPublishedToPosts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:posts) do 6 | add :published, :boolean, default: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20150904221251_add_permalink_unique_index_to_posts.exs: -------------------------------------------------------------------------------- 1 | defmodule Dash.Repo.Migrations.AddPermalinkUniqueIndexToPosts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create index(:posts, [:permalink], unique: true) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /priv/repo/migrations/20150907212409_add_published_at_and_summary_to_posts.exs: -------------------------------------------------------------------------------- 1 | defmodule Dash.Repo.Migrations.AddPublishedAtAndSummaryToPosts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:posts) do 6 | add :published_at, :utc_datetime 7 | add :summary, :text 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20150917184838_add_users_table.exs: -------------------------------------------------------------------------------- 1 | defmodule Dash.Repo.Migrations.AddUsersTable do 2 | use Ecto.Migration 3 | 4 | def change do 5 | execute "CREATE EXTENSION IF NOT EXISTS hstore" 6 | 7 | create table(:users) do 8 | add :name, :string 9 | add :bio, :text 10 | add :nickname, :string 11 | add :social, :hstore 12 | 13 | timestamps() 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/repo/migrations/20150917191613_add_user_to_posts.exs: -------------------------------------------------------------------------------- 1 | defmodule Dash.Repo.Migrations.AddUserToPosts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:posts) do 6 | add :user_id, :integer 7 | end 8 | 9 | create index(:posts, [:user_id]) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # Dash.Repo.insert!(%Dash.SomeModel{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /rel/config.exs: -------------------------------------------------------------------------------- 1 | # Import all plugins from `rel/plugins` 2 | # They can then be used by adding `plugin MyPlugin` to 3 | # either an environment, or release definition, where 4 | # `MyPlugin` is the name of the plugin module. 5 | Path.join(["rel", "plugins", "*.exs"]) 6 | |> Path.wildcard() 7 | |> Enum.map(&Code.eval_file(&1)) 8 | 9 | use Mix.Releases.Config, 10 | # This sets the default release built by `mix release` 11 | default_release: :default, 12 | # This sets the default environment used by `mix release` 13 | default_environment: Mix.env() 14 | 15 | # For a full list of config options for both releases 16 | # and environments, visit https://hexdocs.pm/distillery/configuration.html 17 | 18 | 19 | # You may define one or more environments in this file, 20 | # an environment's settings will override those of a release 21 | # when building in that environment, this combination of release 22 | # and environment configuration is called a profile 23 | 24 | environment :dev do 25 | set dev_mode: true 26 | set include_erts: false 27 | # set cookie: :"4oM?.IH%F0mD{mA~l~zY0?_%=L_|>_;Lc=4Ngm*:ud=*r7XoXY>h7,OJ8d~s2mRG" 28 | end 29 | 30 | environment :prod do 31 | plugin PhoenixDigestTask 32 | set include_erts: true 33 | set include_src: false 34 | # set cookie: :"?,SE9^?H]mIfshg`}Z$.xr3c 8 | info output 9 | Mix.Task.run("phoenix.digest") 10 | nil 11 | {output, error_code} -> 12 | {:error, output, error_code} 13 | end 14 | end 15 | 16 | # def after_assembly(%Release{} = _release) do 17 | # info "after assembly!" 18 | # nil 19 | # end 20 | 21 | # def before_package(%Release{} = _release) do 22 | # info "before package!" 23 | # nil 24 | # end 25 | 26 | # def after_package(%Release{} = _release) do 27 | # info "after package!" 28 | # nil 29 | # end 30 | 31 | # def after_cleanup(%Release{} = _release) do 32 | # info "after cleanup!" 33 | # nil 34 | # end 35 | end 36 | -------------------------------------------------------------------------------- /test/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dash.PageControllerTest do 2 | use Dash.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get conn, "/" 6 | assert html_response(conn, 200) =~ "Posts" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/controllers/post_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dash.PostControllerTest do 2 | use Dash.ConnCase 3 | 4 | alias Dash.Post 5 | @valid_attrs %{body: "some content", permalink: "some content", tags: ["some","content"] , title: "some content"} 6 | @invalid_attrs %{} 7 | 8 | setup do 9 | conn = build_conn() 10 | {:ok, conn: conn} 11 | end 12 | 13 | test "lists all entries on index", %{conn: conn} do 14 | conn = get conn, post_path(conn, :index) 15 | assert html_response(conn, 200) =~ "Posts" 16 | end 17 | 18 | test "renders form for new resources", %{conn: conn} do 19 | conn = get conn, post_path(conn, :new) 20 | assert html_response(conn, 200) =~ "New post" 21 | end 22 | 23 | test "creates resource and redirects when data is valid", %{conn: conn} do 24 | conn = post conn, post_path(conn, :create), post: @valid_attrs 25 | assert redirected_to(conn) == post_path(conn, :index) 26 | assert Repo.get_by(Post, @valid_attrs) 27 | end 28 | 29 | test "does not create resource and renders errors when data is invalid", %{conn: conn} do 30 | conn = post conn, post_path(conn, :create), post: @invalid_attrs 31 | assert html_response(conn, 200) =~ "New post" 32 | end 33 | 34 | test "shows chosen resource", %{conn: conn} do 35 | post_changeset = Post.changeset( 36 | %Post{}, %{title: "Foo"} 37 | ) 38 | post = Repo.insert! post_changeset 39 | conn = get conn, post_path(conn, :show, post) 40 | assert html_response(conn, 200) =~ "Foo" 41 | end 42 | 43 | test "renders page not found when id is nonexistent", %{conn: conn} do 44 | assert_raise Ecto.NoResultsError, fn -> 45 | get conn, post_path(conn, :show, -1) 46 | end 47 | end 48 | 49 | test "renders form for editing chosen resource", %{conn: conn} do 50 | post = Repo.insert! %Post{} 51 | conn = get conn, post_path(conn, :edit, post) 52 | assert html_response(conn, 200) =~ "Edit post" 53 | end 54 | 55 | test "updates chosen resource and redirects when data is valid", %{conn: conn} do 56 | post = Repo.insert! %Post{} 57 | conn = put conn, post_path(conn, :update, post), post: @valid_attrs 58 | assert redirected_to(conn) == post_path(conn, :show, post) 59 | assert Repo.get_by(Post, @valid_attrs) 60 | end 61 | 62 | test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do 63 | post = Repo.insert! %Post{} 64 | conn = put conn, post_path(conn, :update, post), post: @invalid_attrs 65 | assert html_response(conn, 200) =~ "Edit post" 66 | end 67 | 68 | test "deletes chosen resource", %{conn: conn} do 69 | post = Repo.insert! %Post{} 70 | conn = delete conn, post_path(conn, :delete, post) 71 | assert redirected_to(conn) == post_path(conn, :index) 72 | refute Repo.get(Post, post.id) 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/models/post_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dash.PostTest do 2 | use Dash.ModelCase 3 | 4 | alias Dash.Post 5 | 6 | @valid_attrs %{body: "some content", permalink: "some content", tags: ["some", "content"], title: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Post.changeset(%Post{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Post.changeset(%Post{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build 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 Dash.Repo 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query 27 | 28 | 29 | # The default endpoint for testing 30 | @endpoint Dash.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Dash.Repo) 36 | 37 | unless tags[:async] do 38 | Ecto.Adapters.SQL.Sandbox.mode(Dash.Repo, {:shared, self()}) 39 | end 40 | 41 | :ok 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build 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 Dash.Repo 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query 27 | 28 | import Dash.Router.Helpers 29 | 30 | # The default endpoint for testing 31 | @endpoint Dash.Endpoint 32 | end 33 | end 34 | 35 | setup tags do 36 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Dash.Repo) 37 | 38 | unless tags[:async] do 39 | Ecto.Adapters.SQL.Sandbox.mode(Dash.Repo, {:shared, self()}) 40 | end 41 | 42 | {:ok, conn: Phoenix.ConnTest.build_conn()} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/support/model_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.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 Dash.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import Dash.ModelCase 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Dash.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(Dash.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | 38 | @doc """ 39 | Helper for returning list of errors in a struct when given certain data. 40 | 41 | ## Examples 42 | 43 | Given a User schema that lists `:name` as a required field and validates 44 | `:password` to be safe, it would return: 45 | 46 | iex> errors_on(%User{}, %{password: "password"}) 47 | [password: "is unsafe", name: "is blank"] 48 | 49 | You could then write your assertion like: 50 | 51 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"}) 52 | 53 | You can also create the changeset manually and retrieve the errors 54 | field directly: 55 | 56 | iex> changeset = User.changeset(%User{}, password: "password") 57 | iex> {:password, "is unsafe"} in changeset.errors 58 | true 59 | """ 60 | def errors_on(struct, data) do 61 | struct.__struct__.changeset(struct, data) 62 | |> Ecto.Changeset.traverse_errors(&Dash.ErrorHelpers.translate_error/1) 63 | |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | 3 | Mix.Task.run "ecto.create", ["--quiet"] 4 | Mix.Task.run "ecto.migrate", ["--quiet"] 5 | Ecto.Adapters.SQL.Sandbox.mode(Dash.Repo, :manual) 6 | -------------------------------------------------------------------------------- /test/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dash.ErrorViewTest do 2 | use Dash.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(Dash.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(Dash.ErrorView, "500.html", []) == 14 | "Server internal error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(Dash.ErrorView, "505.html", []) == 19 | "Server internal error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dash.LayoutViewTest do 2 | use Dash.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /test/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dash.PageViewTest do 2 | use Dash.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", Dash.RoomChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "users_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # Dash.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /web/controllers/atom_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.AtomController do 2 | use Dash.Web, :controller 3 | 4 | import Ecto.Query, only: [from: 2] 5 | 6 | alias Dash.Post 7 | 8 | def index(conn, _params) do 9 | query = from p in Post, where: p.published == true, 10 | preload: [:user], 11 | order_by: [desc: p.published_at] 12 | 13 | posts = Repo.all(query) 14 | 15 | conn 16 | |> put_layout(false) 17 | |> put_resp_content_type("text/xml") 18 | |> render("index.html", posts: posts) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.PageController do 2 | use Dash.Web, :controller 3 | 4 | import Ecto.Query, only: [from: 2] 5 | 6 | alias Dash.Post 7 | 8 | def index(conn, _params) do 9 | query = from p in Post, where: p.published == true, 10 | preload: [:user], 11 | order_by: [desc: p.published_at] 12 | 13 | [first|posts] = Repo.all(query) 14 | render(conn, "index.html", first: first, posts: posts) 15 | end 16 | 17 | def show(conn, %{"permalink" => permalink}) do 18 | post = Repo.get_by!(Post, permalink: permalink) |> Repo.preload(:user) 19 | render(conn, "show.html", post: post) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /web/controllers/post_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.PostController do 2 | use Dash.Web, :controller 3 | 4 | alias Dash.Post 5 | 6 | plug :scrub_params, "post" when action in [:create, :update] 7 | 8 | def index(conn, _params) do 9 | query = from p in Post, order_by: [desc: p.updated_at] 10 | posts = Repo.all(query) 11 | render(conn, "index.html", posts: posts) 12 | end 13 | 14 | def new(conn, _params) do 15 | changeset = Post.changeset(%Post{}) 16 | render(conn, "new.html", changeset: changeset) 17 | end 18 | 19 | def create(conn, %{"post" => post_params}) do 20 | changeset = Post.changeset(%Post{}, post_params) 21 | 22 | case Repo.insert(changeset) do 23 | {:ok, _post} -> 24 | conn 25 | |> put_flash(:info, "Post created successfully.") 26 | |> redirect(to: post_path(conn, :index)) 27 | {:error, changeset} -> 28 | render(conn, "new.html", changeset: changeset) 29 | end 30 | end 31 | 32 | def show(conn, %{"id" => id}) do 33 | post = Repo.get!(Post, id) |> Repo.preload(:user) 34 | render(conn, "show.html", post: post) 35 | end 36 | 37 | def edit(conn, %{"id" => id}) do 38 | post = Repo.get!(Post, id) 39 | changeset = Post.changeset(post) 40 | render(conn, "edit.html", post: post, changeset: changeset) 41 | end 42 | 43 | def update(conn, %{"id" => id, "post" => post_params}) do 44 | post = Repo.get!(Post, id) 45 | changeset = Post.changeset(post, post_params) 46 | 47 | case Repo.update(changeset) do 48 | {:ok, post} -> 49 | conn 50 | |> put_flash(:info, "Post updated successfully.") 51 | |> redirect(to: post_path(conn, :show, post)) 52 | {:error, changeset} -> 53 | render(conn, "edit.html", post: post, changeset: changeset) 54 | end 55 | end 56 | 57 | def delete(conn, %{"id" => id}) do 58 | post = Repo.get!(Post, id) 59 | 60 | # Here we use delete! (with a bang) because we expect 61 | # it to always work (and if it does not, it will raise). 62 | Repo.delete!(post) 63 | 64 | conn 65 | |> put_flash(:info, "Post deleted successfully.") 66 | |> redirect(to: post_path(conn, :index)) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /web/controllers/sitemap_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.SitemapController do 2 | use Dash.Web, :controller 3 | 4 | import Ecto.Query, only: [from: 2] 5 | 6 | alias Dash.Post 7 | 8 | def index(conn, _params) do 9 | query = from p in Post, where: p.published == true, 10 | order_by: [desc: p.published_at] 11 | 12 | posts = Repo.all(query) 13 | 14 | conn 15 | |> put_layout(false) 16 | |> put_resp_content_type("text/xml") 17 | |> render("index.html", posts: posts) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import Dash.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :dash 24 | end 25 | -------------------------------------------------------------------------------- /web/models/post.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.Post do 2 | use Dash.Web, :model 3 | 4 | schema "posts" do 5 | field :title, :string 6 | field :summary, :string 7 | field :body, :string 8 | field :permalink, :string 9 | field :tags, {:array, :string} 10 | field :published, :boolean, default: false 11 | field :published_at, Ecto.DateTime 12 | field :tags_text, :string, virtual: true 13 | 14 | belongs_to :user, Dash.User 15 | 16 | timestamps() 17 | end 18 | 19 | @required_fields ~w(title) 20 | @optional_fields ~w(body permalink tags summary published published_at user_id) 21 | 22 | @doc """ 23 | Creates a changeset based on the `model` and `params`. 24 | 25 | If no params are provided, an invalid changeset is returned 26 | with no validation performed. 27 | """ 28 | def changeset(post, params \\ %{}) do 29 | post 30 | |> cast(params, [:title, :body, :permalink, :tags, :summary, :published, :published_at, :user_id]) 31 | |> validate_required([:title]) 32 | |> inflate_tags 33 | |> update_published 34 | end 35 | 36 | def update_published(changeset) do 37 | case {changeset.changes[:published], changeset.params["published"]} do 38 | {false, true} -> put_published_at(changeset) 39 | _ -> changeset 40 | end 41 | end 42 | 43 | def put_published_at(changeset) do 44 | change(changeset, 45 | %{published_at: Ecto.DateTime.utc, published: true}) 46 | end 47 | 48 | def inflate_tags(changeset) do 49 | case changeset.params["tags_text"] do 50 | nil -> changeset 51 | _ -> 52 | new_tags = String.split(changeset.params["tags_text"], ",", trim: true) 53 | |> Enum.map(&String.strip/1) 54 | 55 | put_change(changeset, :tags, new_tags) 56 | end 57 | end 58 | 59 | @doc """ 60 | Flatten a `tags` field into a string separated by comma and sets new 61 | value on `tags_text` virtual field. `tags` field is 62 | a Postgresql array. 63 | 64 | If `tags` field is empty, this method sets an empty string. 65 | """ 66 | def flat_tags(post) do 67 | flatten = case post.tags do 68 | [_|_] -> Enum.join(post.tags, ", ") 69 | _ -> "" 70 | end 71 | 72 | Map.put(post, :tags_text, flatten) 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /web/models/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.User do 2 | use Dash.Web, :model 3 | 4 | schema "users" do 5 | field :name, :string 6 | field :bio, :string 7 | field :nickname, :string 8 | field :social, :map 9 | 10 | has_many :posts, Dash.Post 11 | 12 | timestamps() 13 | end 14 | 15 | @required_fields ~w(name nickname) 16 | @optional_fields ~w(bio social) 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 \\ :empty) do 25 | model 26 | |> cast(params, @required_fields, @optional_fields) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.Router do 2 | use Dash.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 | end 15 | 16 | pipeline :xml do 17 | plug :accepts, ["xml"] 18 | end 19 | 20 | scope "/", Dash do 21 | pipe_through :xml 22 | 23 | get "/atom.xml", AtomController, :index 24 | get "/sitemap.xml", SitemapController, :index 25 | end 26 | 27 | scope "/", Dash do 28 | pipe_through :browser # Use the default browser stack 29 | 30 | resources "/#{Application.get_env(:dash, :admin_path)}", PostController 31 | get "/", PageController, :index 32 | get "/:permalink", PageController, :show 33 | 34 | end 35 | 36 | # Other scopes may use custom stacks. 37 | # scope "/api", Dash do 38 | # pipe_through :api 39 | # end 40 | end 41 | -------------------------------------------------------------------------------- /web/static/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michelada/dash/dce8e85c07b093c2cb8880290a461a7fc76d2e7d/web/static/assets/favicon.ico -------------------------------------------------------------------------------- /web/static/assets/fonts/2FD706_0_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michelada/dash/dce8e85c07b093c2cb8880290a461a7fc76d2e7d/web/static/assets/fonts/2FD706_0_0.eot -------------------------------------------------------------------------------- /web/static/assets/fonts/2FD706_0_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michelada/dash/dce8e85c07b093c2cb8880290a461a7fc76d2e7d/web/static/assets/fonts/2FD706_0_0.ttf -------------------------------------------------------------------------------- /web/static/assets/fonts/2FD706_0_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michelada/dash/dce8e85c07b093c2cb8880290a461a7fc76d2e7d/web/static/assets/fonts/2FD706_0_0.woff -------------------------------------------------------------------------------- /web/static/assets/fonts/2FD706_0_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michelada/dash/dce8e85c07b093c2cb8880290a461a7fc76d2e7d/web/static/assets/fonts/2FD706_0_0.woff2 -------------------------------------------------------------------------------- /web/static/assets/images/ic-mail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ic_mail 5 | Created with sketchtool. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /web/static/assets/images/main-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michelada/dash/dce8e85c07b093c2cb8880290a461a7fc76d2e7d/web/static/assets/images/main-logo.png -------------------------------------------------------------------------------- /web/static/assets/images/mario_chavez.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michelada/dash/dce8e85c07b093c2cb8880290a461a7fc76d2e7d/web/static/assets/images/mario_chavez.jpg -------------------------------------------------------------------------------- /web/static/assets/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michelada/dash/dce8e85c07b093c2cb8880290a461a7fc76d2e7d/web/static/assets/images/phoenix.png -------------------------------------------------------------------------------- /web/static/assets/images/sprites.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sprites 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /web/static/assets/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /web/static/css/_font.scss: -------------------------------------------------------------------------------- 1 | @import url("//hello.myfonts.net/count/2fd706"); 2 | 3 | @font-face {font-family: 'ITCAvantGardeW04-Bold';src: url('/fonts/2FD706_0_0.eot');src: url('/fonts/2FD706_0_0.eot?#iefix') format('embedded-opentype'),url('/fonts/2FD706_0_0.woff2') format('woff2'),url('/fonts/2FD706_0_0.woff') format('woff'),url('/fonts/2FD706_0_0.ttf') format('truetype');} 4 | -------------------------------------------------------------------------------- /web/static/css/_mixings.scss: -------------------------------------------------------------------------------- 1 | @mixin mq-tablet($orientation: '') { 2 | @include mq(1024px, $orientation) { @content; } 3 | } 4 | 5 | @mixin mq-mobile-plus($orientation: '') { 6 | @include mq(736px, $orientation) { @content; } 7 | } 8 | 9 | @mixin mq-mobile($orientation: '') { 10 | @include mq(667px, $orientation) { @content; } 11 | } 12 | 13 | @mixin mq($max-width, $orientation: '') { 14 | @if $orientation == '' { 15 | @media only screen and (max-width: #{$max-width}) { @content; } 16 | } @else { 17 | @media only screen and (max-width: #{$max-width}) and (orientation: #{$orientation}) { @content; } 18 | } 19 | } 20 | 21 | @mixin transition-prop($property: all, $duration: .25s) { 22 | -webkit-transition-property: $property; 23 | transition-property: $property; 24 | -webkit-transition-duration: $duration; 25 | transition-duration: $duration; 26 | } 27 | 28 | @mixin font-family($family: $base-font) { 29 | font-family: $family; 30 | text-rendering: geometricPrecision; 31 | -webkit-font-smoothing: antialiased; 32 | } 33 | 34 | @mixin transition($property: all, $time: 0.5s) { 35 | -webkit-transition: $property $time ease; 36 | -moz-transition: $property $time ease; 37 | -ms-transition: $property $time ease; 38 | transition: $property $time ease; 39 | } 40 | 41 | @mixin delay($time: 0.5s) { 42 | -webkit-transition-delay: $time; 43 | -moz-transition-delay: $time; 44 | -o-transition-delay: $time; 45 | transition-delay: $time; 46 | } 47 | -------------------------------------------------------------------------------- /web/static/css/_variables.scss: -------------------------------------------------------------------------------- 1 | $blue: #15162f; 2 | $blue-light: #292c52; 3 | $black: #2d2d2d; 4 | $gray: #F6F6F6; 5 | $yellow: #f9d026; 6 | $red: #ea3434; 7 | 8 | $base-font: "Droid Sans", sans-serif; 9 | $alternate-font: ITCAvantGardeW04-Bold, serif; 10 | -------------------------------------------------------------------------------- /web/static/css/admin.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'mixings'; 3 | 4 | .admin { 5 | header { 6 | background-color: white; 7 | border-bottom: 1px solid $gray; 8 | 9 | a { 10 | color: $blue; 11 | 12 | &:hover { 13 | color: $blue; 14 | } 15 | } 16 | } 17 | 18 | h2 { 19 | margin-top: 160px; 20 | color: $blue; 21 | } 22 | 23 | h3 { 24 | margin-bottom: 0; 25 | } 26 | 27 | .primary-button { 28 | margin-top: 3em; 29 | padding: .8em 1.6em; 30 | border: 1px solid white; 31 | padding-left: 1em; 32 | color: white; 33 | background-color: $blue; 34 | transition: all .5s ease; 35 | 36 | &:hover { 37 | background-color: white; 38 | border: 1px solid $blue; 39 | } 40 | } 41 | 42 | .container { 43 | max-width: 1200px; 44 | margin: 0 auto 3em auto; 45 | } 46 | 47 | .pure-table { 48 | width: 100%; 49 | 50 | .permalink { 51 | color: $blue; 52 | margin-top: 0.5em; 53 | margin: 0 54 | } 55 | 56 | .tags { 57 | font-size: 0.8em; 58 | margin-top: 0; 59 | } 60 | 61 | form.link { 62 | display: inline-block; 63 | float: right; 64 | } 65 | } 66 | 67 | .post-view { 68 | margin: 120px auto; 69 | 70 | width: 90%; 71 | max-width: 700px; 72 | } 73 | 74 | .form-group, .form-group-1-2 { 75 | margin-bottom: 1.5em; 76 | 77 | .summary { 78 | height: 10em; 79 | } 80 | 81 | .body { 82 | height: 50em; 83 | } 84 | } 85 | 86 | .form-group-1-2 { 87 | display: inline-block; 88 | width: 49.8%; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /web/static/css/app.scss: -------------------------------------------------------------------------------- 1 | @import 'font'; 2 | @import 'variables'; 3 | @import 'mixings'; 4 | 5 | html { 6 | height: 100%; 7 | box-sizing: border-box; 8 | } 9 | 10 | body { 11 | @include font-family; 12 | font-size: 16px; 13 | line-height: 24px; 14 | color: $black; 15 | 16 | @include mq-mobile { 17 | width: 700px; 18 | } 19 | } 20 | 21 | h1, h2, h3, h4 { 22 | @include font-family($alternate-font); 23 | font-weight: 700; 24 | } 25 | 26 | a { 27 | color: $red; 28 | text-decoration: none; 29 | 30 | @include transition-prop(color); 31 | 32 | &:hover { 33 | color: $yellow; 34 | } 35 | } 36 | 37 | header { 38 | position: fixed; 39 | top: 0; 40 | 41 | border-bottom: 1px solid white; 42 | background-color: $blue; 43 | 44 | height: 90px; 45 | width: 100%; 46 | 47 | z-index: 2; 48 | } 49 | 50 | nav { 51 | width: 90%; 52 | max-width: 1120px; 53 | margin: 0 auto; 54 | 55 | .contact { 56 | background-color: $yellow; 57 | border-radius: 100px; 58 | padding: 15px 50px 10px 25px; 59 | background-image: url(/images/ic-mail.svg); 60 | background-repeat: no-repeat; 61 | background-position: 88%; 62 | background-size: 20px; 63 | width: 7em; 64 | color: $blue; 65 | 66 | &:hover { 67 | background-color: lighten($yellow, 10%); 68 | } 69 | 70 | @include mq-mobile { 71 | margin: 0 auto; 72 | } 73 | } 74 | 75 | .menu-toggle { 76 | display: none; 77 | } 78 | 79 | .michelada { 80 | display: inline-block; 81 | width: 270px; 82 | padding-top: 18px; 83 | 84 | span { 85 | position: relative; 86 | } 87 | 88 | .logo { 89 | fill: transparent; 90 | height: 56px; 91 | width: 56px; 92 | margin-right: 15px; 93 | 94 | position: relative; 95 | float: left; 96 | } 97 | 98 | .text { 99 | height: 15px; 100 | width: 185px; 101 | top: 20px; 102 | 103 | position: relative; 104 | } 105 | } 106 | 107 | ul { 108 | display: inline-block; 109 | float: right; 110 | padding-top: 5px; 111 | padding-left: 1em; 112 | 113 | list-style-type: none; 114 | 115 | @include font-family($alternate-font); 116 | 117 | li { 118 | float: left; 119 | 120 | margin-left: 2em; 121 | font-size: 1.2em; 122 | 123 | &:first-child { 124 | margin-left: 0; 125 | 126 | a { 127 | padding-top: 18px; 128 | display: block; 129 | 130 | @include mq-mobile { 131 | margin-top: 1em; 132 | } 133 | } 134 | } 135 | 136 | a { 137 | color: white; 138 | 139 | &:hover { 140 | color: white; 141 | } 142 | } 143 | } 144 | } 145 | 146 | .lets-talk { 147 | margin-top: 3em; 148 | padding: .8em 1.6em; 149 | border: 1px solid white; 150 | padding-left: 1em; 151 | color: white; 152 | transition: background-color .5s ease; 153 | 154 | &:hover { 155 | background-color: white; 156 | color: $blue; 157 | } 158 | } 159 | 160 | @include mq(917px) { 161 | .menu-toggle { 162 | width: 34px; 163 | height: 34px; 164 | position: absolute; 165 | top: 28px; 166 | right: 15px; 167 | display: block !important; 168 | 169 | .bar { 170 | background-color: white; 171 | display: block; 172 | width: 25px; 173 | height: 3px; 174 | border-radius: 100px; 175 | position: absolute; 176 | top: 18px; 177 | right: 7px; 178 | @include transition; 179 | 180 | &:first-child { 181 | -webkit-transform: translateY(-8px); 182 | -moz-transform: translateY(-8px); 183 | -ms-transform: translateY(-8px); 184 | transform: translateY(-8px); 185 | } 186 | } 187 | } 188 | 189 | .home-menu { 190 | display: block; 191 | width: 100%; 192 | height: 0; 193 | overflow: hidden; 194 | margin: 0; 195 | padding: 0; 196 | position: absolute; 197 | left: 0; 198 | top: 90px; 199 | box-sizing: border-box; 200 | 201 | z-index: 3; 202 | background-color: $blue; 203 | @include transition(height); 204 | 205 | li { 206 | float: none; 207 | margin-left: 0; 208 | text-align: center; 209 | margin-bottom: 0.5em; 210 | font-size: 1.5em; 211 | opacity: 0; 212 | @include transition(opacity); 213 | 214 | &:last-child { 215 | margin-top: 1.6em; 216 | } 217 | } 218 | 219 | .lets-talk { 220 | padding: .5em .8em; 221 | border: 1px solid white; 222 | color: $blue; 223 | background-color: white; 224 | } 225 | } 226 | 227 | .menu-open { 228 | display: block; 229 | height: 200px; 230 | 231 | li { 232 | opacity: 1; 233 | } 234 | } 235 | 236 | .toggle-open { 237 | .bar { 238 | -webkit-transform: rotate(45deg); 239 | -moz-transform: rotate(45deg); 240 | -ms-transform: rotate(45deg); 241 | transform: rotate(45deg); 242 | } 243 | 244 | .bar:first-child { 245 | -webkit-transform: rotate(-45deg); 246 | -moz-transform: rotate(-45deg); 247 | -ms-transform: rotate(-45deg); 248 | transform: rotate(-45deg); 249 | } 250 | } 251 | 252 | ul { 253 | display: none; 254 | } 255 | } 256 | } 257 | 258 | .container { 259 | width: 90%; 260 | max-width: 700px; 261 | } 262 | 263 | .posts, .post-view { 264 | margin: 160px auto; 265 | 266 | .author, .time { 267 | @include font-family($alternate-font); 268 | } 269 | 270 | .author { 271 | color: darken($gray, 50%); 272 | } 273 | 274 | .time { 275 | color: $blue; 276 | margin-left: 0.5em; 277 | text-transform: uppercase; 278 | 279 | &:before { 280 | content: '|>'; 281 | margin-right: 0.5em; 282 | } 283 | } 284 | 285 | .content { 286 | font-size: 1.2em; 287 | margin-top: 2.5em; 288 | line-height: 1.8em; 289 | 290 | h2, h3 { 291 | margin-top: 1.5em; 292 | } 293 | 294 | pre { 295 | border: 1px solid darken($gray, 20%); 296 | border-radius: 2px; 297 | 298 | background-color: $gray; 299 | padding: 1em; 300 | 301 | font-size: 0.9em; 302 | line-height: 1.6em; 303 | } 304 | 305 | em, .inline { 306 | font-family: Inconsolata, monospace, sans-serif; 307 | font-style: normal; 308 | 309 | background-color: $gray; 310 | padding: 2px 5px; 311 | border-radius: 2px; 312 | } 313 | 314 | a { 315 | border-bottom: 1px solid $blue; 316 | padding-bottom: 2px; 317 | } 318 | 319 | th, td { 320 | box-sizing: border-box; 321 | padding: 5px 10px; 322 | border: 1px solid darken($gray, 20%); 323 | } 324 | 325 | th { 326 | background-color: #F6F6F6; 327 | } 328 | 329 | .share { 330 | margin-top: 18px; 331 | padding: 2px 10px 2px; 332 | display: inline-block; 333 | border: 1px solid $gray; 334 | border-radius: 1px; 335 | 336 | svg { 337 | fill: $black; 338 | margin-right: 2px; 339 | position: relative; 340 | top: 4px; 341 | width: 20px; 342 | height: 20px; 343 | } 344 | 345 | &:hover { 346 | border: 1px solid $blue; 347 | 348 | svg { 349 | fill: $blue; 350 | } 351 | } 352 | } 353 | } 354 | } 355 | 356 | .post-view { 357 | margin-bottom: 60px; 358 | 359 | h1 { 360 | margin-bottom: 0.5em; 361 | line-height: 1.3em; 362 | font-size: 3.5em; 363 | 364 | @include mq-mobile { 365 | line-height: 1.3em; 366 | margin-bottom: 0.5em; 367 | } 368 | } 369 | 370 | .author, .time { 371 | font-size: 1.2em; 372 | } 373 | 374 | .content { 375 | font-size: 1.2em; 376 | margin-top: 2.5em; 377 | line-height: 1.8em; 378 | } 379 | 380 | .tags { 381 | margin-top: 5px; 382 | } 383 | 384 | .tag { 385 | @include font-family($alternate-font); 386 | 387 | background-color: $gray; 388 | padding: 3px 8px; 389 | margin-right: 10px; 390 | } 391 | } 392 | 393 | .posts { 394 | .post { 395 | &:not(:last-child) { 396 | padding-bottom: 30px; 397 | margin-bottom: 32px; 398 | 399 | border-bottom: 1px solid darken($gray, 15%); 400 | } 401 | 402 | h2 { 403 | margin: .2em 0; 404 | 405 | a { 406 | font-size: 1.5em; 407 | text-decoration: none; 408 | border-bottom: none; 409 | } 410 | } 411 | } 412 | } 413 | 414 | .social { 415 | .twitter, .github { 416 | width: 30px; 417 | height: 30px; 418 | fill: darken($gray, 20%); 419 | } 420 | 421 | .twitter { 422 | margin-top: 3px; 423 | } 424 | 425 | a { 426 | width: 30px; 427 | height: 30px; 428 | display: block; 429 | float: left; 430 | margin-right: 1em; 431 | 432 | &:hover { 433 | svg { 434 | fill: $blue; 435 | } 436 | } 437 | } 438 | } 439 | 440 | footer { 441 | background-color: $blue-light; 442 | min-height: 5em; 443 | 444 | .container { 445 | margin: 0 auto; 446 | padding: 3em 0; 447 | 448 | box-sizing: border-box; 449 | 450 | width: 90%; 451 | max-width: 1120px; 452 | } 453 | 454 | .marker { 455 | display: inline-block; 456 | margin-bottom: 36px; 457 | margin-right: 10px; 458 | 459 | width: 20px; 460 | height: 30px; 461 | fill: white; 462 | } 463 | 464 | p { 465 | @include font-family($base-font); 466 | display: inline-block; 467 | 468 | color: white; 469 | 470 | b { 471 | text-transform: uppercase; 472 | } 473 | } 474 | 475 | .built-with { 476 | 477 | p { 478 | display: block; 479 | margin-bottom: 0; 480 | 481 | text-align: center; 482 | font-size: 0.9em; 483 | line-height: 1em; 484 | } 485 | 486 | a { 487 | color: $yellow; 488 | text-decoration: none; 489 | 490 | &:hover { 491 | color: $yellow; 492 | } 493 | } 494 | } 495 | 496 | .social { 497 | padding-top: 2.5em; 498 | padding-right: 1em; 499 | box-sizing: border-box; 500 | 501 | a { 502 | float: right; 503 | margin-right: 2em; 504 | } 505 | } 506 | 507 | a { 508 | color: transparent; 509 | 510 | &:hover { 511 | color: transparent; 512 | } 513 | } 514 | 515 | @include mq-mobile-plus { 516 | .michelada, .build-with, .social { 517 | text-align: center; 518 | } 519 | 520 | .built-with { 521 | margin-top: 0; 522 | 523 | p { 524 | font-size: 1.1em; 525 | } 526 | } 527 | 528 | .social { 529 | a { 530 | float: none; 531 | display: inline-block; 532 | 533 | &:last-child { 534 | margin-right: 0px; 535 | } 536 | } 537 | } 538 | } 539 | } 540 | 541 | .author-info { 542 | hr { 543 | width: 40%; 544 | margin: 4em auto; 545 | border-top: 0; 546 | border-bottom: 1px solid $blue; 547 | } 548 | 549 | table { 550 | box-sizing: border-box; 551 | width: 100%; 552 | 553 | .picture { 554 | text-align: right; 555 | padding-right: 1em; 556 | } 557 | 558 | .frame { 559 | display: inline-block; 560 | border-radius: 50%; 561 | overflow: hidden; 562 | width: 150px; 563 | height: 150px; 564 | 565 | img { 566 | width: 150px; 567 | } 568 | } 569 | 570 | h3 { 571 | color: $blue; 572 | font-weight: 300; 573 | font-size: 1.3em; 574 | line-height: 1.8em; 575 | margin: 0; 576 | } 577 | 578 | p { 579 | margin-top: 0; 580 | } 581 | 582 | .social { 583 | a { 584 | height: 25px; 585 | width: 25px; 586 | margin-right: 0.5em; 587 | } 588 | } 589 | } 590 | } 591 | 592 | #disqus_thread { 593 | margin-top: 6em; 594 | } 595 | -------------------------------------------------------------------------------- /web/static/js/app.js: -------------------------------------------------------------------------------- 1 | // Brunch automatically concatenates all files in your 2 | // watched paths. Those paths can be configured at 3 | // config.paths.watched in "brunch-config.js". 4 | // 5 | // However, those files will only be executed if 6 | // explicitly imported. The only exception are files 7 | // in vendor, which are never wrapped in imports and 8 | // therefore are always executed. 9 | 10 | // Import dependencies 11 | // 12 | // If you no longer want to use a dependency, remember 13 | // to also remove its path from "config.paths.watched". 14 | import "deps/phoenix_html/web/static/js/phoenix_html" 15 | 16 | // Import local files 17 | // 18 | // Local files can be imported directly using relative 19 | // paths "./socket" or full ones "web/static/js/socket". 20 | 21 | // import socket from "./socket" 22 | 23 | import {Menu} from "./menu" 24 | 25 | $(function() { 26 | var menu = new Menu('.home-menu', '.menu-toggle'); 27 | 28 | $('.share').click(function(event) { 29 | var width = 575, 30 | height = 400, 31 | left = ($(window).width() - width) / 2, 32 | top = ($(window).height() - height) / 2, 33 | url = this.href, 34 | opts = 'status=1' + 35 | ',width=' + width + 36 | ',height=' + height + 37 | ',top=' + top + 38 | ',left=' + left; 39 | 40 | window.open(url, 'twitter', opts); 41 | 42 | return false; 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /web/static/js/menu.coffee: -------------------------------------------------------------------------------- 1 | class Menu 2 | constructor:(menu, menu_toggle) -> 3 | @$menu = $(menu) 4 | @$menu_toggle = $(menu_toggle) 5 | 6 | observe_event = if 'onorientationchange' in window then 'orientationchange' else 'resize' 7 | 8 | $(window).on observe_event, @close_menu 9 | @$menu_toggle.on 'click', @toggle_menu 10 | @$menu.find('a').on 'click', @toggle_menu 11 | 12 | close_menu: (event) => 13 | if @$menu.hasClass('menu-open') 14 | @toggle_menu event 15 | 16 | toggle_menu: (event) => 17 | @$menu.toggleClass 'menu-open' 18 | @$menu_toggle.toggleClass 'toggle-open' 19 | 20 | module.exports = 21 | Menu: Menu 22 | -------------------------------------------------------------------------------- /web/static/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "web/static/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket 5 | // and connect at the socket path in "lib/my_app/endpoint.ex": 6 | import {Socket} from "phoenix" 7 | 8 | let socket = new Socket("/socket", {params: {token: window.userToken}}) 9 | 10 | // When you connect, you'll often need to authenticate the client. 11 | // For example, imagine you have an authentication plug, `MyAuth`, 12 | // which authenticates the session and assigns a `:current_user`. 13 | // If the current user exists you can assign the user's token in 14 | // the connection for use in the layout. 15 | // 16 | // In your "web/router.ex": 17 | // 18 | // pipeline :browser do 19 | // ... 20 | // plug MyAuth 21 | // plug :put_user_token 22 | // end 23 | // 24 | // defp put_user_token(conn, _) do 25 | // if current_user = conn.assigns[:current_user] do 26 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id) 27 | // assign(conn, :user_token, token) 28 | // else 29 | // conn 30 | // end 31 | // end 32 | // 33 | // Now you need to pass this token to JavaScript. You can do so 34 | // inside a script tag in "web/templates/layout/app.html.eex": 35 | // 36 | // 37 | // 38 | // You will need to verify the user token in the "connect/2" function 39 | // in "web/channels/user_socket.ex": 40 | // 41 | // def connect(%{"token" => token}, socket) do 42 | // # max_age: 1209600 is equivalent to two weeks in seconds 43 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do 44 | // {:ok, user_id} -> 45 | // {:ok, assign(socket, :user, user_id)} 46 | // {:error, reason} -> 47 | // :error 48 | // end 49 | // end 50 | // 51 | // Finally, pass the token on connect as below. Or remove it 52 | // from connect if you don't care about authentication. 53 | 54 | socket.connect() 55 | 56 | // Now that you are connected, you can join channels with a topic: 57 | let channel = socket.channel("topic:subtopic", {}) 58 | channel.join() 59 | .receive("ok", resp => { console.log("Joined successfully", resp) }) 60 | .receive("error", resp => { console.log("Unable to join", resp) }) 61 | 62 | export default socket 63 | -------------------------------------------------------------------------------- /web/static/vendor/css/grids-responsive-min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Pure v0.6.0 3 | Copyright 2014 Yahoo! Inc. All rights reserved. 4 | Licensed under the BSD License. 5 | https://github.com/yahoo/pure/blob/master/LICENSE.md 6 | */ 7 | @media screen and (min-width:35.5em){.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-1-2,.pure-u-sm-1-3,.pure-u-sm-2-3,.pure-u-sm-1-4,.pure-u-sm-3-4,.pure-u-sm-1-5,.pure-u-sm-2-5,.pure-u-sm-3-5,.pure-u-sm-4-5,.pure-u-sm-5-5,.pure-u-sm-1-6,.pure-u-sm-5-6,.pure-u-sm-1-8,.pure-u-sm-3-8,.pure-u-sm-5-8,.pure-u-sm-7-8,.pure-u-sm-1-12,.pure-u-sm-5-12,.pure-u-sm-7-12,.pure-u-sm-11-12,.pure-u-sm-1-24,.pure-u-sm-2-24,.pure-u-sm-3-24,.pure-u-sm-4-24,.pure-u-sm-5-24,.pure-u-sm-6-24,.pure-u-sm-7-24,.pure-u-sm-8-24,.pure-u-sm-9-24,.pure-u-sm-10-24,.pure-u-sm-11-24,.pure-u-sm-12-24,.pure-u-sm-13-24,.pure-u-sm-14-24,.pure-u-sm-15-24,.pure-u-sm-16-24,.pure-u-sm-17-24,.pure-u-sm-18-24,.pure-u-sm-19-24,.pure-u-sm-20-24,.pure-u-sm-21-24,.pure-u-sm-22-24,.pure-u-sm-23-24,.pure-u-sm-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-sm-1-24{width:4.1667%;*width:4.1357%}.pure-u-sm-1-12,.pure-u-sm-2-24{width:8.3333%;*width:8.3023%}.pure-u-sm-1-8,.pure-u-sm-3-24{width:12.5%;*width:12.469%}.pure-u-sm-1-6,.pure-u-sm-4-24{width:16.6667%;*width:16.6357%}.pure-u-sm-1-5{width:20%;*width:19.969%}.pure-u-sm-5-24{width:20.8333%;*width:20.8023%}.pure-u-sm-1-4,.pure-u-sm-6-24{width:25%;*width:24.969%}.pure-u-sm-7-24{width:29.1667%;*width:29.1357%}.pure-u-sm-1-3,.pure-u-sm-8-24{width:33.3333%;*width:33.3023%}.pure-u-sm-3-8,.pure-u-sm-9-24{width:37.5%;*width:37.469%}.pure-u-sm-2-5{width:40%;*width:39.969%}.pure-u-sm-5-12,.pure-u-sm-10-24{width:41.6667%;*width:41.6357%}.pure-u-sm-11-24{width:45.8333%;*width:45.8023%}.pure-u-sm-1-2,.pure-u-sm-12-24{width:50%;*width:49.969%}.pure-u-sm-13-24{width:54.1667%;*width:54.1357%}.pure-u-sm-7-12,.pure-u-sm-14-24{width:58.3333%;*width:58.3023%}.pure-u-sm-3-5{width:60%;*width:59.969%}.pure-u-sm-5-8,.pure-u-sm-15-24{width:62.5%;*width:62.469%}.pure-u-sm-2-3,.pure-u-sm-16-24{width:66.6667%;*width:66.6357%}.pure-u-sm-17-24{width:70.8333%;*width:70.8023%}.pure-u-sm-3-4,.pure-u-sm-18-24{width:75%;*width:74.969%}.pure-u-sm-19-24{width:79.1667%;*width:79.1357%}.pure-u-sm-4-5{width:80%;*width:79.969%}.pure-u-sm-5-6,.pure-u-sm-20-24{width:83.3333%;*width:83.3023%}.pure-u-sm-7-8,.pure-u-sm-21-24{width:87.5%;*width:87.469%}.pure-u-sm-11-12,.pure-u-sm-22-24{width:91.6667%;*width:91.6357%}.pure-u-sm-23-24{width:95.8333%;*width:95.8023%}.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-5-5,.pure-u-sm-24-24{width:100%}}@media screen and (min-width:48em){.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-1-2,.pure-u-md-1-3,.pure-u-md-2-3,.pure-u-md-1-4,.pure-u-md-3-4,.pure-u-md-1-5,.pure-u-md-2-5,.pure-u-md-3-5,.pure-u-md-4-5,.pure-u-md-5-5,.pure-u-md-1-6,.pure-u-md-5-6,.pure-u-md-1-8,.pure-u-md-3-8,.pure-u-md-5-8,.pure-u-md-7-8,.pure-u-md-1-12,.pure-u-md-5-12,.pure-u-md-7-12,.pure-u-md-11-12,.pure-u-md-1-24,.pure-u-md-2-24,.pure-u-md-3-24,.pure-u-md-4-24,.pure-u-md-5-24,.pure-u-md-6-24,.pure-u-md-7-24,.pure-u-md-8-24,.pure-u-md-9-24,.pure-u-md-10-24,.pure-u-md-11-24,.pure-u-md-12-24,.pure-u-md-13-24,.pure-u-md-14-24,.pure-u-md-15-24,.pure-u-md-16-24,.pure-u-md-17-24,.pure-u-md-18-24,.pure-u-md-19-24,.pure-u-md-20-24,.pure-u-md-21-24,.pure-u-md-22-24,.pure-u-md-23-24,.pure-u-md-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-md-1-24{width:4.1667%;*width:4.1357%}.pure-u-md-1-12,.pure-u-md-2-24{width:8.3333%;*width:8.3023%}.pure-u-md-1-8,.pure-u-md-3-24{width:12.5%;*width:12.469%}.pure-u-md-1-6,.pure-u-md-4-24{width:16.6667%;*width:16.6357%}.pure-u-md-1-5{width:20%;*width:19.969%}.pure-u-md-5-24{width:20.8333%;*width:20.8023%}.pure-u-md-1-4,.pure-u-md-6-24{width:25%;*width:24.969%}.pure-u-md-7-24{width:29.1667%;*width:29.1357%}.pure-u-md-1-3,.pure-u-md-8-24{width:33.3333%;*width:33.3023%}.pure-u-md-3-8,.pure-u-md-9-24{width:37.5%;*width:37.469%}.pure-u-md-2-5{width:40%;*width:39.969%}.pure-u-md-5-12,.pure-u-md-10-24{width:41.6667%;*width:41.6357%}.pure-u-md-11-24{width:45.8333%;*width:45.8023%}.pure-u-md-1-2,.pure-u-md-12-24{width:50%;*width:49.969%}.pure-u-md-13-24{width:54.1667%;*width:54.1357%}.pure-u-md-7-12,.pure-u-md-14-24{width:58.3333%;*width:58.3023%}.pure-u-md-3-5{width:60%;*width:59.969%}.pure-u-md-5-8,.pure-u-md-15-24{width:62.5%;*width:62.469%}.pure-u-md-2-3,.pure-u-md-16-24{width:66.6667%;*width:66.6357%}.pure-u-md-17-24{width:70.8333%;*width:70.8023%}.pure-u-md-3-4,.pure-u-md-18-24{width:75%;*width:74.969%}.pure-u-md-19-24{width:79.1667%;*width:79.1357%}.pure-u-md-4-5{width:80%;*width:79.969%}.pure-u-md-5-6,.pure-u-md-20-24{width:83.3333%;*width:83.3023%}.pure-u-md-7-8,.pure-u-md-21-24{width:87.5%;*width:87.469%}.pure-u-md-11-12,.pure-u-md-22-24{width:91.6667%;*width:91.6357%}.pure-u-md-23-24{width:95.8333%;*width:95.8023%}.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-5-5,.pure-u-md-24-24{width:100%}}@media screen and (min-width:64em){.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-1-2,.pure-u-lg-1-3,.pure-u-lg-2-3,.pure-u-lg-1-4,.pure-u-lg-3-4,.pure-u-lg-1-5,.pure-u-lg-2-5,.pure-u-lg-3-5,.pure-u-lg-4-5,.pure-u-lg-5-5,.pure-u-lg-1-6,.pure-u-lg-5-6,.pure-u-lg-1-8,.pure-u-lg-3-8,.pure-u-lg-5-8,.pure-u-lg-7-8,.pure-u-lg-1-12,.pure-u-lg-5-12,.pure-u-lg-7-12,.pure-u-lg-11-12,.pure-u-lg-1-24,.pure-u-lg-2-24,.pure-u-lg-3-24,.pure-u-lg-4-24,.pure-u-lg-5-24,.pure-u-lg-6-24,.pure-u-lg-7-24,.pure-u-lg-8-24,.pure-u-lg-9-24,.pure-u-lg-10-24,.pure-u-lg-11-24,.pure-u-lg-12-24,.pure-u-lg-13-24,.pure-u-lg-14-24,.pure-u-lg-15-24,.pure-u-lg-16-24,.pure-u-lg-17-24,.pure-u-lg-18-24,.pure-u-lg-19-24,.pure-u-lg-20-24,.pure-u-lg-21-24,.pure-u-lg-22-24,.pure-u-lg-23-24,.pure-u-lg-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-lg-1-24{width:4.1667%;*width:4.1357%}.pure-u-lg-1-12,.pure-u-lg-2-24{width:8.3333%;*width:8.3023%}.pure-u-lg-1-8,.pure-u-lg-3-24{width:12.5%;*width:12.469%}.pure-u-lg-1-6,.pure-u-lg-4-24{width:16.6667%;*width:16.6357%}.pure-u-lg-1-5{width:20%;*width:19.969%}.pure-u-lg-5-24{width:20.8333%;*width:20.8023%}.pure-u-lg-1-4,.pure-u-lg-6-24{width:25%;*width:24.969%}.pure-u-lg-7-24{width:29.1667%;*width:29.1357%}.pure-u-lg-1-3,.pure-u-lg-8-24{width:33.3333%;*width:33.3023%}.pure-u-lg-3-8,.pure-u-lg-9-24{width:37.5%;*width:37.469%}.pure-u-lg-2-5{width:40%;*width:39.969%}.pure-u-lg-5-12,.pure-u-lg-10-24{width:41.6667%;*width:41.6357%}.pure-u-lg-11-24{width:45.8333%;*width:45.8023%}.pure-u-lg-1-2,.pure-u-lg-12-24{width:50%;*width:49.969%}.pure-u-lg-13-24{width:54.1667%;*width:54.1357%}.pure-u-lg-7-12,.pure-u-lg-14-24{width:58.3333%;*width:58.3023%}.pure-u-lg-3-5{width:60%;*width:59.969%}.pure-u-lg-5-8,.pure-u-lg-15-24{width:62.5%;*width:62.469%}.pure-u-lg-2-3,.pure-u-lg-16-24{width:66.6667%;*width:66.6357%}.pure-u-lg-17-24{width:70.8333%;*width:70.8023%}.pure-u-lg-3-4,.pure-u-lg-18-24{width:75%;*width:74.969%}.pure-u-lg-19-24{width:79.1667%;*width:79.1357%}.pure-u-lg-4-5{width:80%;*width:79.969%}.pure-u-lg-5-6,.pure-u-lg-20-24{width:83.3333%;*width:83.3023%}.pure-u-lg-7-8,.pure-u-lg-21-24{width:87.5%;*width:87.469%}.pure-u-lg-11-12,.pure-u-lg-22-24{width:91.6667%;*width:91.6357%}.pure-u-lg-23-24{width:95.8333%;*width:95.8023%}.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-5-5,.pure-u-lg-24-24{width:100%}}@media screen and (min-width:80em){.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-1-2,.pure-u-xl-1-3,.pure-u-xl-2-3,.pure-u-xl-1-4,.pure-u-xl-3-4,.pure-u-xl-1-5,.pure-u-xl-2-5,.pure-u-xl-3-5,.pure-u-xl-4-5,.pure-u-xl-5-5,.pure-u-xl-1-6,.pure-u-xl-5-6,.pure-u-xl-1-8,.pure-u-xl-3-8,.pure-u-xl-5-8,.pure-u-xl-7-8,.pure-u-xl-1-12,.pure-u-xl-5-12,.pure-u-xl-7-12,.pure-u-xl-11-12,.pure-u-xl-1-24,.pure-u-xl-2-24,.pure-u-xl-3-24,.pure-u-xl-4-24,.pure-u-xl-5-24,.pure-u-xl-6-24,.pure-u-xl-7-24,.pure-u-xl-8-24,.pure-u-xl-9-24,.pure-u-xl-10-24,.pure-u-xl-11-24,.pure-u-xl-12-24,.pure-u-xl-13-24,.pure-u-xl-14-24,.pure-u-xl-15-24,.pure-u-xl-16-24,.pure-u-xl-17-24,.pure-u-xl-18-24,.pure-u-xl-19-24,.pure-u-xl-20-24,.pure-u-xl-21-24,.pure-u-xl-22-24,.pure-u-xl-23-24,.pure-u-xl-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-xl-1-24{width:4.1667%;*width:4.1357%}.pure-u-xl-1-12,.pure-u-xl-2-24{width:8.3333%;*width:8.3023%}.pure-u-xl-1-8,.pure-u-xl-3-24{width:12.5%;*width:12.469%}.pure-u-xl-1-6,.pure-u-xl-4-24{width:16.6667%;*width:16.6357%}.pure-u-xl-1-5{width:20%;*width:19.969%}.pure-u-xl-5-24{width:20.8333%;*width:20.8023%}.pure-u-xl-1-4,.pure-u-xl-6-24{width:25%;*width:24.969%}.pure-u-xl-7-24{width:29.1667%;*width:29.1357%}.pure-u-xl-1-3,.pure-u-xl-8-24{width:33.3333%;*width:33.3023%}.pure-u-xl-3-8,.pure-u-xl-9-24{width:37.5%;*width:37.469%}.pure-u-xl-2-5{width:40%;*width:39.969%}.pure-u-xl-5-12,.pure-u-xl-10-24{width:41.6667%;*width:41.6357%}.pure-u-xl-11-24{width:45.8333%;*width:45.8023%}.pure-u-xl-1-2,.pure-u-xl-12-24{width:50%;*width:49.969%}.pure-u-xl-13-24{width:54.1667%;*width:54.1357%}.pure-u-xl-7-12,.pure-u-xl-14-24{width:58.3333%;*width:58.3023%}.pure-u-xl-3-5{width:60%;*width:59.969%}.pure-u-xl-5-8,.pure-u-xl-15-24{width:62.5%;*width:62.469%}.pure-u-xl-2-3,.pure-u-xl-16-24{width:66.6667%;*width:66.6357%}.pure-u-xl-17-24{width:70.8333%;*width:70.8023%}.pure-u-xl-3-4,.pure-u-xl-18-24{width:75%;*width:74.969%}.pure-u-xl-19-24{width:79.1667%;*width:79.1357%}.pure-u-xl-4-5{width:80%;*width:79.969%}.pure-u-xl-5-6,.pure-u-xl-20-24{width:83.3333%;*width:83.3023%}.pure-u-xl-7-8,.pure-u-xl-21-24{width:87.5%;*width:87.469%}.pure-u-xl-11-12,.pure-u-xl-22-24{width:91.6667%;*width:91.6357%}.pure-u-xl-23-24{width:95.8333%;*width:95.8023%}.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-5-5,.pure-u-xl-24-24{width:100%}} -------------------------------------------------------------------------------- /web/static/vendor/css/pure-min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Pure v0.6.0 3 | Copyright 2014 Yahoo! Inc. All rights reserved. 4 | Licensed under the BSD License. 5 | https://github.com/yahoo/pure/blob/master/LICENSE.md 6 | */ 7 | /*! 8 | normalize.css v^3.0 | MIT License | git.io/normalize 9 | Copyright (c) Nicolas Gallagher and Jonathan Neal 10 | */ 11 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap;-ms-align-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class *="pure-u"]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%;*width:4.1357%}.pure-u-1-12,.pure-u-2-24{width:8.3333%;*width:8.3023%}.pure-u-1-8,.pure-u-3-24{width:12.5%;*width:12.469%}.pure-u-1-6,.pure-u-4-24{width:16.6667%;*width:16.6357%}.pure-u-1-5{width:20%;*width:19.969%}.pure-u-5-24{width:20.8333%;*width:20.8023%}.pure-u-1-4,.pure-u-6-24{width:25%;*width:24.969%}.pure-u-7-24{width:29.1667%;*width:29.1357%}.pure-u-1-3,.pure-u-8-24{width:33.3333%;*width:33.3023%}.pure-u-3-8,.pure-u-9-24{width:37.5%;*width:37.469%}.pure-u-2-5{width:40%;*width:39.969%}.pure-u-5-12,.pure-u-10-24{width:41.6667%;*width:41.6357%}.pure-u-11-24{width:45.8333%;*width:45.8023%}.pure-u-1-2,.pure-u-12-24{width:50%;*width:49.969%}.pure-u-13-24{width:54.1667%;*width:54.1357%}.pure-u-7-12,.pure-u-14-24{width:58.3333%;*width:58.3023%}.pure-u-3-5{width:60%;*width:59.969%}.pure-u-5-8,.pure-u-15-24{width:62.5%;*width:62.469%}.pure-u-2-3,.pure-u-16-24{width:66.6667%;*width:66.6357%}.pure-u-17-24{width:70.8333%;*width:70.8023%}.pure-u-3-4,.pure-u-18-24{width:75%;*width:74.969%}.pure-u-19-24{width:79.1667%;*width:79.1357%}.pure-u-4-5{width:80%;*width:79.969%}.pure-u-5-6,.pure-u-20-24{width:83.3333%;*width:83.3023%}.pure-u-7-8,.pure-u-21-24{width:87.5%;*width:87.469%}.pure-u-11-12,.pure-u-22-24{width:91.6667%;*width:91.6357%}.pure-u-23-24{width:95.8333%;*width:95.8023%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%}.pure-button{display:inline-block;zoom:1;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:0 rgba(0,0,0,0);background-color:#E6E6E6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:hover,.pure-button:focus{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000', GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-moz-linear-gradient(top,rgba(0,0,0,.05) 0,rgba(0,0,0,.1));background-image:-o-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000\9}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{border:0;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);filter:alpha(opacity=40);-khtml-opacity:.4;-moz-opacity:.4;opacity:.4;cursor:not-allowed;box-shadow:none}.pure-button-hidden{display:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129FEA}.pure-form input:not([type]):focus{outline:0;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked input[type=file],.pure-form-stacked select,.pure-form-stacked label,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-aligned .pure-help-inline,.pure-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form input.pure-input-rounded,.pure-form .pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=text],.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message-inline,.pure-form-message{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-list,.pure-menu-item{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-link,.pure-menu-heading{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-separator{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-allow-hover:hover>.pure-menu-children,.pure-menu-active>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar{display:none}.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-link,.pure-menu-disabled,.pure-menu-heading{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:hover,.pure-menu-link:focus{background-color:#eee}.pure-menu-selected .pure-menu-link,.pure-menu-selected .pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} -------------------------------------------------------------------------------- /web/templates/atom/index.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | michelada.io Blog 4 | 5 | <%= utc_date(List.last(@posts).published_at) %> 6 | 7 | michelada.io 8 | 9 | © 2017 michelada.io 10 | <%= gettext("We work with startups and established companies implementing great ideas with simplicity and code quality") %> 11 | <%= static_url(@conn, "/images/main-logo.png") %> 12 | 13 | <%= for post <- @posts do %> 14 | 15 | <%= post.title %> 16 | 17 | <%= page_url(@conn, :show, post.permalink) %> 18 | <%= utc_date(post.updated_at) %> 19 | <%= utc_date(post.published_at) %> 20 | <%= post.summary %> 21 | 22 | <%= author_name(post.user) %> 23 | 24 | 25 | <% end %> 26 | 27 | -------------------------------------------------------------------------------- /web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= render_meta(assigns) %> 5 | 6 | " /> 7 | 8 | 9 | 10 | "> 11 | <%= render_existing view_module(@conn), "styles.html", assigns %> 12 | 22 | 23 | 24 | 25 | 26 | <%= render_existing view_module(@conn), "menu.html", assigns %> 27 | 28 | <%= render @view_module, @view_template, assigns %> 29 | 30 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /web/templates/layout/meta.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | "> 6 | 7 | 8 | 9 | 10 | "> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | "> 19 | 20 | 21 | 22 | michelada.io :: <%= page_title(@conn, assigns) %> 23 | -------------------------------------------------------------------------------- /web/templates/page/author.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 10 | 23 | 24 |
6 |
7 | " /> 8 |
9 |
11 |

<%= @author.name %>

12 |

<%= @author.bio %>

13 | 22 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

<%= @first.title %>

4 | <%= "By #{author_name(@first.user)}" %> 5 | <%= human_date(@first.published_at) %> 6 |

<%= markdown_to_html(@first.body) %>

7 |
8 | 9 | <%= for post <- @posts do %> 10 |
11 |

<%= post.title %>

12 | <%= "By #{author_name(post.user)}" %> 13 | <%= human_date(post.published_at) %> 14 |

<%= post.summary %>

15 |
16 | <% end %> 17 |
18 | -------------------------------------------------------------------------------- /web/templates/page/menu.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 28 |
29 | -------------------------------------------------------------------------------- /web/templates/page/show.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

<%= @post.title %>

3 | <%= "By #{author_name(@post.user)}" %> 4 | <%= human_date(@post.published_at) %> 5 |

6 | <%= for tag <- @post.tags do %> 7 | <%= tag %> 8 | <% end %> 9 |

10 |
11 | <%= markdown_to_html(@post.body) %> 12 | 18 |
19 | 20 | <%= render_author(@conn, @post.user) %> 21 | 22 |
23 | 35 | 36 |
37 | -------------------------------------------------------------------------------- /web/templates/post/edit.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

Edit post

3 | 4 | <%= render "form.html", changeset: @changeset, 5 | action: post_path(@conn, :update, @post) %> 6 | 7 |
8 | -------------------------------------------------------------------------------- /web/templates/post/form.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, @action, [class: "pure-form pure-form-stacked"], fn f -> %> 2 | 3 | <%= if @changeset.action do %> 4 |
5 |

Oops, something went wrong! Please check the errors below:

6 | 11 |
12 | <% end %> 13 | 14 |
15 | <%= label f, :title, "Title", class: "control-label" %> 16 | <%= text_input f, :title, class: "pure-input-1" %> 17 |
18 | 19 |
20 | <%= label f, :permalink, "Permalink", class: "control-label" %> 21 | <%= text_input f, :permalink, class: "pure-input-1" %> 22 |
23 | 24 |
25 | <%= label f, :tags_text, "Tags", class: "control-label" %> 26 | <%= text_input f, :tags_text, class: "pure-input-1" %> 27 |
28 | 29 |
30 | <%= label f, :summary, "Summary", class: "control-label" %> 31 | <%= textarea f, :summary, class: "pure-input-1 summary" %> 32 |
33 | 34 |
35 | <%= label f, :body, "Body", class: "control-label" %> 36 | <%= textarea f, :body, class: "pure-input-1 body" %> 37 |
38 | 39 |
40 | <%= submit "Submit", class: "pure-button" %> 41 |
42 | <% end %> 43 | -------------------------------------------------------------------------------- /web/templates/post/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

Posts

3 | 4 | 5 | 6 | <%= for post <- @posts do %> 7 | 8 | 21 | 22 | <% end %> 23 | 24 |
9 |

10 | <%= link post.title, to: post_path(@conn, :show, post) %> 11 | <%= published(post) %> 12 |

13 | 14 |

<%= flat_tags(post) %>

15 |

<%= post.summary %>

16 |
17 | <%= link "Edit", to: post_path(@conn, :edit, post), class: "btn btn-default btn-xs" %> 18 | <%= link "Delete", to: post_path(@conn, :delete, post), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %> 19 |
20 |
25 | 26 |
27 | -------------------------------------------------------------------------------- /web/templates/post/menu.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 25 |
26 | -------------------------------------------------------------------------------- /web/templates/post/new.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

New post

3 | 4 | <%= render "form.html", changeset: @changeset, 5 | action: post_path(@conn, :create) %> 6 | 7 |
8 | -------------------------------------------------------------------------------- /web/templates/post/show.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

<%= @post.title %>

3 | <%= "By #{author_name(@post.user)}" %> 4 | <%= human_date(@post.published_at) %> 5 |

6 | <% if @post.tags do %> 7 | <%= for tag <- @post.tags do %> 8 | <%= tag %> 9 | <% end %> 10 | <% end %> 11 |

12 |
13 | <% if @post.body do %> 14 | <%= markdown_to_html(@post.body) %> 15 | <% end %> 16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /web/templates/sitemap/index.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= page_url(@conn, :index) %> 5 | weekly 6 | 7 | <%= for post <- @posts do %> 8 | 9 | <%= page_url(@conn, :show, post.permalink) %> 10 | weekly 11 | 12 | <% end %> 13 | 14 | -------------------------------------------------------------------------------- /web/views/atom_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.AtomView do 2 | use Dash.Web, :view 3 | 4 | def utc_date(date) do 5 | {:ok, date} = Ecto.DateTime.dump(date) 6 | Chronos.Formatter.strftime(date, "%Y-%m-%dT%H:%M:%SZ") 7 | end 8 | 9 | def author_name(user) when user == nil do 10 | "michelada.io" 11 | end 12 | 13 | def author_name(user) do 14 | user.name 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | if error = form.errors[field] do 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. 25 | # Ecto will pass the :count keyword if the error message is 26 | # meant to be pluralized. 27 | # On your own code and templates, depending on whether you 28 | # need the message to be pluralized or not, this could be 29 | # written simply as: 30 | # 31 | # dngettext "errors", "1 file", "%{count} files", count 32 | # dgettext "errors", "is invalid" 33 | # 34 | if count = opts[:count] do 35 | Gettext.dngettext(Dash.Gettext, "errors", msg, msg, count, opts) 36 | else 37 | Gettext.dgettext(Dash.Gettext, "errors", msg, opts) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.ErrorView do 2 | use Dash.Web, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Server internal error" 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render "500.html", assigns 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.LayoutView do 2 | use Dash.Web, :view 3 | 4 | def render_meta(assigns) do 5 | render_existing(Dash.LayoutView, "meta.html", assigns) 6 | end 7 | 8 | def page_title(_conn, assigns) do 9 | case assigns[:post] do 10 | nil -> gettext("We plan, code and launch awesome web and mobile products.") 11 | _ -> assigns[:post].title 12 | end 13 | end 14 | 15 | def page_description(_conn, assigns) do 16 | case assigns[:post] do 17 | nil -> gettext("We work with startups and established companies implementing great ideas with simplicity and code quality") 18 | _ -> assigns[:post].summary 19 | end 20 | end 21 | 22 | def current_page_url(conn, assigns) do 23 | path = case assigns[:post] do 24 | nil -> "/" 25 | _ -> 26 | permalink = assigns[:post].permalink || "" 27 | "/" <> permalink 28 | end 29 | 30 | static_url(conn, path) 31 | end 32 | 33 | def is_admin?(conn) do 34 | case view_module(conn) do 35 | Dash.PostView -> true 36 | _ -> false 37 | end 38 | end 39 | 40 | def add_admin_css_class(conn) do 41 | case is_admin?(conn) do 42 | true -> "admin" 43 | _ -> "" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.PageView do 2 | use Dash.Web, :view 3 | 4 | import Dash.PostHelper 5 | 6 | def render_author(_conn, author) when author == nil do 7 | "" 8 | end 9 | 10 | def render_author(conn, author) do 11 | render_existing(Dash.PageView, "author.html", 12 | %{conn: conn, author: author}) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /web/views/post_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.PostHelper do 2 | import Phoenix.HTML, only: [raw: 1] 3 | 4 | alias Earmark.Options 5 | 6 | def markdown_to_html(content) do 7 | {_, html, _} = Earmark.as_html(content, %Options{gfm: false}) 8 | raw(html) 9 | end 10 | 11 | def human_date(date) when is_nil(date), do: "" 12 | 13 | def human_date(date) do 14 | {:ok, date} = Ecto.DateTime.dump(date) 15 | Chronos.Formatter.strftime(date, "%B %d, %Y") 16 | end 17 | 18 | def author_name(user) when user == nil do 19 | "michelada.io" 20 | end 21 | 22 | def author_name(user) do 23 | user.name 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /web/views/post_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.PostView do 2 | use Dash.Web, :view 3 | 4 | import Dash.PostHelper 5 | 6 | def published(%{published: true}) do 7 | "- (published)" 8 | end 9 | 10 | def published(%{published: false}) do 11 | "" 12 | end 13 | 14 | def render("styles.html", assigns) do 15 | conn = assigns[:conn] 16 | raw "" 17 | end 18 | 19 | def flat_tags(post) do 20 | flatten = case post.tags do 21 | [_|_] -> Enum.join(post.tags, ", ") 22 | _ -> "" 23 | end 24 | flatten 25 | end 26 | 27 | def tags(post) do 28 | tags = case post.tags do 29 | nil -> [] 30 | _ -> post.tags 31 | end 32 | 33 | tags 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /web/views/sitemap_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.SitemapView do 2 | use Dash.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/web.ex: -------------------------------------------------------------------------------- 1 | defmodule Dash.Web do 2 | @moduledoc """ 3 | A module that keeps using definitions for controllers, 4 | views and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use Dash.Web, :controller 9 | use Dash.Web, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. 17 | """ 18 | 19 | def model do 20 | quote do 21 | use Ecto.Schema 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query 26 | end 27 | end 28 | 29 | def controller do 30 | quote do 31 | use Phoenix.Controller 32 | 33 | alias Dash.Repo 34 | import Ecto 35 | import Ecto.Query 36 | 37 | import Dash.Router.Helpers 38 | import Dash.Gettext 39 | end 40 | end 41 | 42 | def view do 43 | quote do 44 | use Phoenix.View, root: "web/templates" 45 | 46 | # Import convenience functions from controllers 47 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] 48 | 49 | # Use all HTML functionality (forms, tags, etc) 50 | use Phoenix.HTML 51 | 52 | import Dash.Router.Helpers 53 | import Dash.ErrorHelpers 54 | import Dash.Gettext 55 | end 56 | end 57 | 58 | def router do 59 | quote do 60 | use Phoenix.Router 61 | end 62 | end 63 | 64 | def channel do 65 | quote do 66 | use Phoenix.Channel 67 | 68 | alias Dash.Repo 69 | import Ecto 70 | import Ecto.Query 71 | import Dash.Gettext 72 | end 73 | end 74 | 75 | @doc """ 76 | When used, dispatch to the appropriate controller/view/etc. 77 | """ 78 | defmacro __using__(which) when is_atom(which) do 79 | apply(__MODULE__, which, []) 80 | end 81 | end 82 | --------------------------------------------------------------------------------