├── .tool-versions ├── .formatter.exs ├── test ├── test_helper.exs ├── landing_page_web │ ├── views │ │ ├── page_view_test.exs │ │ ├── layout_view_test.exs │ │ └── error_view_test.exs │ └── controllers │ │ ├── page_controller_test.exs │ │ └── v1 │ │ └── lead_controller_test.exs ├── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── data_case.ex └── landing_page │ └── marketing │ └── marketing_test.exs ├── lib ├── landing_page_web │ ├── views │ │ ├── page_view.ex │ │ ├── layout_view.ex │ │ ├── error_view.ex │ │ └── error_helpers.ex │ ├── controllers │ │ ├── page_controller.ex │ │ ├── fallback_controller.ex │ │ └── v1 │ │ │ └── lead_controller.ex │ ├── router.ex │ ├── templates │ │ ├── page │ │ │ └── index.html.eex │ │ └── layout │ │ │ └── app.html.eex │ ├── gettext.ex │ ├── channels │ │ └── user_socket.ex │ └── endpoint.ex ├── landing_page.ex ├── landing_page │ ├── repo.ex │ ├── marketing │ │ ├── lead.ex │ │ └── marketing.ex │ └── application.ex └── landing_page_web.ex ├── assets ├── static │ ├── favicon.ico │ └── robots.txt ├── css │ ├── global │ │ ├── _skin.sass │ │ ├── _layout.sass │ │ ├── _base.sass │ │ ├── _typography.sass │ │ ├── _settings.sass │ │ └── _utilities.sass │ ├── modules │ │ ├── _modules.sass │ │ └── pages │ │ │ └── _landing.sass │ ├── libs │ │ └── _libs-variable-overrides.sass │ └── app.sass ├── elm │ ├── src │ │ ├── Ports.elm │ │ ├── Messages.elm │ │ ├── Decoders.elm │ │ ├── Main.elm │ │ ├── Model.elm │ │ ├── Commands.elm │ │ ├── Update.elm │ │ └── View.elm │ └── elm-package.json ├── package.json ├── js │ └── app.js └── brunch-config.js ├── priv ├── repo │ ├── migrations │ │ └── 20171201145808_create_leads.exs │ └── seeds.exs └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot ├── config ├── test.exs ├── config.exs ├── dev.exs └── prod.exs ├── .gitignore ├── README.md ├── mix.exs └── mix.lock /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir ref-master 2 | erlang 20.1 3 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | Ecto.Adapters.SQL.Sandbox.mode(LandingPage.Repo, :manual) 4 | -------------------------------------------------------------------------------- /lib/landing_page_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.PageView do 2 | use LandingPageWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigardone/phoenix-and-elm-landing-page/HEAD/assets/static/favicon.ico -------------------------------------------------------------------------------- /lib/landing_page_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.LayoutView do 2 | use LandingPageWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /test/landing_page_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.PageViewTest do 2 | use LandingPageWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /test/landing_page_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.LayoutViewTest do 2 | use LandingPageWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /assets/css/global/_skin.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - skin 3 | // global skin styles - gradients, colors, box-shadows, etc. 4 | -------------------------------------------------------------------------------- /assets/css/modules/_modules.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - modules 3 | // import new modules here 4 | 5 | @import pages/landing 6 | -------------------------------------------------------------------------------- /assets/css/global/_layout.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - layout 3 | // global layout classes - height, width, padding, margin, etc. 4 | -------------------------------------------------------------------------------- /assets/css/global/_base.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - base 3 | // project defaults for base elements - h1-h6, p, a, etc. 4 | 5 | @include normalize 6 | 7 | -------------------------------------------------------------------------------- /assets/css/global/_typography.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - typography 3 | // global typography styles 4 | 5 | h1, h2, h3, h4 6 | font-family: $header-font-family 7 | -------------------------------------------------------------------------------- /assets/css/global/_settings.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - settings 3 | // global maps and variables 4 | 5 | $purple-dark: darken($purple, 10) 6 | $header-font-family: "Montserrat", sans-serif 7 | 8 | -------------------------------------------------------------------------------- /lib/landing_page_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.PageController do 2 | use LandingPageWeb, :controller 3 | 4 | def index(conn, _params) do 5 | render(conn, "index.html") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /assets/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /assets/elm/src/Ports.elm: -------------------------------------------------------------------------------- 1 | port module Ports exposing (..) 2 | 3 | -- OUT PORTS 4 | 5 | 6 | port initRecaptcha : String -> Cmd msg 7 | 8 | 9 | port resetRecaptcha : () -> Cmd msg 10 | 11 | 12 | 13 | -- IN PORTS 14 | 15 | 16 | port setRecaptchaToken : (String -> msg) -> Sub msg 17 | -------------------------------------------------------------------------------- /test/landing_page_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.PageControllerTest do 2 | use LandingPageWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, "/") 6 | assert html_response(conn, 200) =~ "Phoenix & Elm landing page" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/landing_page.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPage do 2 | @moduledoc """ 3 | LandingPage keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /lib/landing_page/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPage.Repo do 2 | use Ecto.Repo, otp_app: :landing_page 3 | 4 | @doc """ 5 | Dynamically loads the repository url from the 6 | DATABASE_URL environment variable. 7 | """ 8 | def init(_, opts) do 9 | {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /assets/elm/src/Messages.elm: -------------------------------------------------------------------------------- 1 | module Messages exposing (Msg(..)) 2 | 3 | import Dict exposing (Dict) 4 | import Http 5 | 6 | 7 | type Msg 8 | = HandleFullNameInput String 9 | | HandleEmailInput String 10 | | HandleFormSubmit 11 | | SubscribeResponse (Result Http.Error (Dict String String)) 12 | | SetRecaptchaToken String 13 | -------------------------------------------------------------------------------- /assets/css/libs/_libs-variable-overrides.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - library variable overrides 3 | // variable overrides for 3rd party libraries 4 | 5 | $primary: $purple 6 | $input-shadow: 0 0 0 7 | $input-focus-border-color: $grey-light 8 | $input-focus-box-shadow-size: 0 0 0 0 9 | $input-focus-box-shadow-color: transparent 10 | -------------------------------------------------------------------------------- /lib/landing_page_web/controllers/fallback_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.FallbackController do 2 | use LandingPageWeb, :controller 3 | 4 | def call(conn, {:error, %Ecto.Changeset{} = changeset}) do 5 | conn 6 | |> put_status(:unprocessable_entity) 7 | |> render(LandingPageWeb.ErrorView, "error.json", changeset: changeset) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/landing_page_web/controllers/v1/lead_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.V1.LeadController do 2 | use LandingPageWeb, :controller 3 | 4 | alias LandingPage.Marketing 5 | 6 | plug(:scrub_params, "lead") 7 | 8 | def create(conn, %{"lead" => params}) do 9 | with {:ok, lead} <- Marketing.create_lead(params) do 10 | json(conn, lead) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20171201145808_create_leads.exs: -------------------------------------------------------------------------------- 1 | defmodule LandingPage.Repo.Migrations.CreateLeads do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:leads) do 6 | add(:full_name, :string, null: false) 7 | add(:email, :string, null: false) 8 | 9 | timestamps() 10 | end 11 | 12 | create(unique_index(:leads, [:email])) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /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 | # LandingPage.Repo.insert!(%LandingPage.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /assets/css/global/_utilities.sass: -------------------------------------------------------------------------------- 1 | // - - - - - - - - - - - - - - - - - - - 2 | // - - utilities 3 | // global extends, mixins, functions, and utility classes 4 | 5 | // - - - - - - - - - - - - - - - - - - - 6 | // - - extends 7 | 8 | // - - - - - - - - - - - - - - - - - - - 9 | // - - mixins 10 | 11 | // - - - - - - - - - - - - - - - - - - - 12 | // - - functions 13 | 14 | // - - - - - - - - - - - - - - - - - - - 15 | // - - utilities 16 | -------------------------------------------------------------------------------- /assets/elm/src/Decoders.elm: -------------------------------------------------------------------------------- 1 | module Decoders exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Json.Decode as Decode 5 | import Model exposing (ValidationErrors) 6 | 7 | 8 | responseDecoder : Decode.Decoder (Dict String String) 9 | responseDecoder = 10 | Decode.dict Decode.string 11 | 12 | 13 | validationErrorsDecoder : Decode.Decoder ValidationErrors 14 | validationErrorsDecoder = 15 | Decode.dict <| Decode.list Decode.string 16 | -------------------------------------------------------------------------------- /assets/elm/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Repo for my Phoenix and Elm subscription form tutorial series", 4 | "repository": "https://github.com/bigardone/phoenix-and-elm-subscription-form.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 12 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 13 | "elm-lang/http": "1.0.0 <= v < 2.0.0" 14 | }, 15 | "elm-version": "0.18.0 <= v < 0.19.0" 16 | } 17 | -------------------------------------------------------------------------------- /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 :landing_page, LandingPageWeb.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 :landing_page, LandingPage.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: "postgres", 16 | password: "postgres", 17 | database: "landing_page_test", 18 | hostname: "localhost", 19 | pool: Ecto.Adapters.SQL.Sandbox 20 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "license": "MIT", 4 | "scripts": { 5 | "deploy": "brunch build --production", 6 | "watch": "brunch watch --stdin" 7 | }, 8 | "dependencies": { 9 | "bulma": "^0.6.1", 10 | "normalize-scss": "^7.0.0", 11 | "phoenix": "file:../deps/phoenix", 12 | "phoenix_html": "file:../deps/phoenix_html" 13 | }, 14 | "devDependencies": { 15 | "babel-brunch": "6.1.1", 16 | "brunch": "2.10.9", 17 | "clean-css-brunch": "2.10.0", 18 | "elm-brunch": "^0.10.0", 19 | "sass-brunch": "^2.10.4", 20 | "uglify-js-brunch": "2.10.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/landing_page_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.ErrorView do 2 | use LandingPageWeb, :view 3 | 4 | import LandingPageWeb.ErrorHelpers 5 | 6 | def render("404.html", _assigns) do 7 | "Page not found" 8 | end 9 | 10 | def render("500.html", _assigns) do 11 | "Internal server error" 12 | end 13 | 14 | def render("error.json", %{changeset: changeset}) do 15 | Ecto.Changeset.traverse_errors(changeset, &translate_error/1) 16 | end 17 | 18 | # In case no render clause matches or no 19 | # template is found, let's render it as 500 20 | def template_not_found(_template, assigns) do 21 | render("500.html", assigns) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/landing_page_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.ErrorViewTest do 2 | use LandingPageWeb.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(LandingPageWeb.ErrorView, "404.html", []) == "Page not found" 9 | end 10 | 11 | test "render 500.html" do 12 | assert render_to_string(LandingPageWeb.ErrorView, "500.html", []) == "Internal server error" 13 | end 14 | 15 | test "render any other" do 16 | assert render_to_string(LandingPageWeb.ErrorView, "505.html", []) == "Internal server error" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/landing_page/marketing/lead.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPage.Marketing.Lead do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | alias LandingPage.Marketing.Lead 5 | 6 | @derive {Poison.Encoder, only: [:full_name, :email]} 7 | 8 | schema "leads" do 9 | field(:email, :string) 10 | field(:full_name, :string) 11 | 12 | field(:recaptcha_token, :string, virtual: true) 13 | 14 | timestamps() 15 | end 16 | 17 | @fields ~w(full_name email recaptcha_token)a 18 | 19 | @doc false 20 | def changeset(%Lead{} = lead, attrs) do 21 | lead 22 | |> cast(attrs, @fields) 23 | |> validate_required(@fields) 24 | |> unique_constraint(:email) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # Generated on crash by NPM 11 | npm-debug.log 12 | 13 | # Static artifacts 14 | /assets/node_modules 15 | 16 | # Since we are building assets from assets/, 17 | # we ignore priv/static. You may want to comment 18 | # this depending on your deployment strategy. 19 | /priv/static/ 20 | 21 | # Files matching config/*.secret.exs pattern contain sensitive 22 | # data and you should not commit them into version control. 23 | # 24 | # Alternatively, you may comment the line below and commit the 25 | # secrets files as long as you replace their contents by environment 26 | # variables. 27 | /config/*.secret.exs -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | import Elm from './elm/main'; 2 | 3 | window.onloadCallback = () => { 4 | const formContainer = document.querySelector('#form_container'); 5 | if (formContainer) { 6 | const app = Elm.Main.embed(formContainer); 7 | let recaptcha; 8 | 9 | app.ports.initRecaptcha.subscribe(id => { 10 | window.requestAnimationFrame(() => { 11 | recaptcha = grecaptcha.render(id, { 12 | hl: 'en', 13 | sitekey: '6LcLWzoUAAAAAAkeT3oWaJ3JS2Rxa0E-f0PzC45W', 14 | callback: result => { 15 | app.ports.setRecaptchaToken.send(result); 16 | }, 17 | }); 18 | }); 19 | }); 20 | 21 | app.ports.resetRecaptcha.subscribe(() => { 22 | grecaptcha.reset(recaptcha); 23 | }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /lib/landing_page_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.Router do 2 | use LandingPageWeb, :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 | scope "/", LandingPageWeb do 17 | # Use the default browser stack 18 | pipe_through(:browser) 19 | 20 | get("/", PageController, :index) 21 | end 22 | 23 | # Other scopes may use custom stacks. 24 | scope "/api", LandingPageWeb do 25 | pipe_through(:api) 26 | 27 | scope "/v1", V1 do 28 | post("/leads", LeadController, :create) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/landing_page_web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Phoenix & Elm landing page

5 |

6 | Real use case of building a landing page using Phoenix and Elm, 7 | following some common patterns and best practices. 8 |

9 |
10 |
11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 | -------------------------------------------------------------------------------- /lib/landing_page_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.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 LandingPageWeb.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: :landing_page 24 | end 25 | -------------------------------------------------------------------------------- /assets/elm/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Html exposing (Html) 4 | import Messages exposing (Msg(..)) 5 | import Model exposing (..) 6 | import Ports 7 | import Update exposing (update) 8 | import View exposing (view) 9 | 10 | 11 | main : Program Never Model Msg 12 | main = 13 | Html.program 14 | { init = init 15 | , view = view 16 | , update = update 17 | , subscriptions = subscriptions 18 | } 19 | 20 | 21 | initialModel : Model 22 | initialModel = 23 | { subscribeForm = 24 | Editing 25 | { fullName = "" 26 | , email = "" 27 | , recaptchaToken = Nothing 28 | } 29 | } 30 | 31 | 32 | init : ( Model, Cmd Msg ) 33 | init = 34 | initialModel ! [ Ports.initRecaptcha "recaptcha" ] 35 | 36 | 37 | subscriptions : Model -> Sub Msg 38 | subscriptions model = 39 | Sub.batch 40 | [ Ports.setRecaptchaToken SetRecaptchaToken ] 41 | -------------------------------------------------------------------------------- /test/landing_page_web/controllers/v1/lead_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.V1.LeadControllerTest do 2 | use LandingPageWeb.ConnCase 3 | 4 | describe "POST /api/v1/leads" do 5 | test "returns error response with invalid parms", %{conn: conn} do 6 | conn = post(conn, lead_path(conn, :create), %{"lead" => %{}}) 7 | 8 | assert json_response(conn, 422) == %{ 9 | "full_name" => ["can't be blank"], 10 | "email" => ["can't be blank"], 11 | "recaptcha_token" => ["can't be blank"] 12 | } 13 | end 14 | 15 | test "returns success response with valid params", %{conn: conn} do 16 | params = %{ 17 | "lead" => %{"full_name" => "John", "email" => "foo@bar.com", "recaptcha_token" => "foo"} 18 | } 19 | 20 | conn = post(conn, lead_path(conn, :create), params) 21 | assert json_response(conn, 200) == %{"full_name" => "John", "email" => "foo@bar.com"} 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/landing_page_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Phoenix & Elm landing page 11 | 12 | 13 | "> 14 | 15 | 16 | 17 | <%= render(@view_module, @view_template, assigns) %> 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /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 :landing_page, ecto_repos: [LandingPage.Repo] 10 | 11 | # Configures the endpoint 12 | config :landing_page, LandingPageWeb.Endpoint, 13 | url: [host: "localhost"], 14 | secret_key_base: "sql490bc9EiK7VdO1EPJXkHLKtYUWmHHdOqBlSUv5RFhalJV44wVHwqwc88/CK1y", 15 | render_errors: [view: LandingPageWeb.ErrorView, accepts: ~w(html json)], 16 | pubsub: [name: LandingPage.PubSub, adapter: Phoenix.PubSub.PG2] 17 | 18 | # Configures Elixir's Logger 19 | config :logger, :console, 20 | format: "$time $metadata[$level] $message\n", 21 | metadata: [:request_id] 22 | 23 | # Import environment specific config. This must remain at the bottom 24 | # of this file so it overrides the configuration defined above. 25 | import_config "#{Mix.env()}.exs" 26 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.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 common datastructures and query the data layer. 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 | # The default endpoint for testing 24 | @endpoint LandingPageWeb.Endpoint 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(LandingPage.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(LandingPage.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/landing_page/application.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPage.Application do 2 | use Application 3 | 4 | # See https://hexdocs.pm/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(LandingPage.Repo, []), 13 | # Start the endpoint when the application starts 14 | supervisor(LandingPageWeb.Endpoint, []) 15 | # Start your own worker by calling: LandingPage.Worker.start_link(arg1, arg2, arg3) 16 | # worker(LandingPage.Worker, [arg1, arg2, arg3]), 17 | ] 18 | 19 | # See https://hexdocs.pm/elixir/Supervisor.html 20 | # for other strategies and supported options 21 | opts = [strategy: :one_for_one, name: LandingPage.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 | LandingPageWeb.Endpoint.config_change(changed, removed) 29 | :ok 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.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 common datastructures and query the data layer. 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 | import LandingPageWeb.Router.Helpers 23 | 24 | # The default endpoint for testing 25 | @endpoint LandingPageWeb.Endpoint 26 | end 27 | end 28 | 29 | setup tags do 30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(LandingPage.Repo) 31 | 32 | unless tags[:async] do 33 | Ecto.Adapters.SQL.Sandbox.mode(LandingPage.Repo, {:shared, self()}) 34 | end 35 | 36 | {:ok, conn: Phoenix.ConnTest.build_conn()} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /assets/elm/src/Model.elm: -------------------------------------------------------------------------------- 1 | module Model exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | 5 | 6 | type alias FormFields = 7 | { fullName : String 8 | , email : String 9 | , recaptchaToken : Maybe String 10 | } 11 | 12 | 13 | type alias ValidationErrors = 14 | Dict String (List String) 15 | 16 | 17 | type SubscribeForm 18 | = Editing FormFields 19 | | Saving FormFields 20 | | Invalid FormFields ValidationErrors 21 | | Success FormFields 22 | 23 | 24 | type alias Model = 25 | { subscribeForm : SubscribeForm } 26 | 27 | 28 | extractFormFields : SubscribeForm -> FormFields 29 | extractFormFields subscribeForm = 30 | case subscribeForm of 31 | Editing ff -> 32 | ff 33 | 34 | Saving ff -> 35 | ff 36 | 37 | Invalid ff _ -> 38 | ff 39 | 40 | Success ff -> 41 | ff 42 | 43 | 44 | extractValidationErrors : SubscribeForm -> ValidationErrors 45 | extractValidationErrors subscribeForm = 46 | case subscribeForm of 47 | Invalid _ validationErrors -> 48 | validationErrors 49 | 50 | _ -> 51 | emptyValidationErrors 52 | 53 | 54 | emptyValidationErrors : ValidationErrors 55 | emptyValidationErrors = 56 | Dict.empty 57 | -------------------------------------------------------------------------------- /lib/landing_page_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", LandingPageWeb.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: "user_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # LandingPageWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /assets/elm/src/Commands.elm: -------------------------------------------------------------------------------- 1 | module Commands exposing (subscribe) 2 | 3 | import Dict exposing (Dict) 4 | import Http 5 | import Json.Decode as JD 6 | import Json.Encode as JE 7 | import Decoders exposing (responseDecoder) 8 | import Messages exposing (Msg(..)) 9 | import Model exposing (SubscribeForm(..), FormFields) 10 | 11 | 12 | subscribe : SubscribeForm -> Cmd Msg 13 | subscribe subscribeForm = 14 | case subscribeForm of 15 | Editing formFields -> 16 | Http.send SubscribeResponse (post formFields) 17 | 18 | _ -> 19 | Cmd.none 20 | 21 | 22 | post : FormFields -> Http.Request (Dict String String) 23 | post formFields = 24 | Http.request 25 | { method = "POST" 26 | , headers = [] 27 | , url = "/api/v1/leads" 28 | , body = Http.jsonBody (encodeModel formFields) 29 | , expect = Http.expectJson responseDecoder 30 | , timeout = Nothing 31 | , withCredentials = False 32 | } 33 | 34 | 35 | encodeModel : FormFields -> JD.Value 36 | encodeModel { fullName, email, recaptchaToken } = 37 | JE.object 38 | [ ( "lead" 39 | , JE.object 40 | [ ( "full_name", JE.string fullName ) 41 | , ( "email", JE.string email ) 42 | , ( "recaptcha_token", JE.string <| Maybe.withDefault "" recaptchaToken ) 43 | ] 44 | ) 45 | ] 46 | -------------------------------------------------------------------------------- /lib/landing_page_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn error -> 13 | content_tag(:span, translate_error(error), class: "help-block") 14 | end) 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # 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(LandingPageWeb.Gettext, "errors", msg, msg, count, opts) 36 | else 37 | Gettext.dgettext(LandingPageWeb.Gettext, "errors", msg, opts) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phoenix and Elm landing page 2 | 3 | ![http://codeloveandboards.com/images/blog/phoenix-elm-landing-page/landing-page-9e2448ef.jpg](http://codeloveandboards.com/images/blog/phoenix-elm-landing-page/landing-page-9e2448ef.jpg) 4 | 5 | ## Tutorial 6 | 7 | 1. [Bootstrapping the project and the basic API functionality to save our first leads](http://codeloveandboards.com/blog/2017/12/02/phoenix-elm-landing-page-pt-1/) 8 | 2. [Building the landing page UI and the basic Elm subscription form](http://codeloveandboards.com/blog/2017/12/23/phoenix-elm-landing-page-pt-2/) 9 | 3. [Adding Google reCAPTCHA support to avoid spambots](http://codeloveandboards.com/blog/2018/01/06/phoenix-elm-landing-page-pt-3/) 10 | 11 | ## Instructions 12 | To start your Phoenix server: 13 | 14 | * Install dependencies with `mix deps.get` 15 | * Create and migrate your database with `mix ecto.create && mix ecto.migrate` 16 | * Install Node.js dependencies with `cd assets && npm install` 17 | * Start Phoenix endpoint with `mix phx.server` 18 | 19 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 20 | 21 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment). 22 | 23 | ## Learn more 24 | 25 | * Official website: http://www.phoenixframework.org/ 26 | * Guides: http://phoenixframework.org/docs/overview 27 | * Docs: https://hexdocs.pm/phoenix 28 | * Mailing list: http://groups.google.com/group/phoenix-talk 29 | * Source: https://github.com/phoenixframework/phoenix 30 | -------------------------------------------------------------------------------- /test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPage.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 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 LandingPage.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import LandingPage.DataCase 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(LandingPage.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(LandingPage.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | 38 | @doc """ 39 | A helper that transform changeset errors to a map of messages. 40 | 41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 42 | assert "password is too short" in errors_on(changeset).password 43 | assert %{password: ["password is too short"]} = errors_on(changeset) 44 | 45 | """ 46 | def errors_on(changeset) do 47 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 48 | Enum.reduce(opts, message, fn {key, value}, acc -> 49 | String.replace(acc, "%{#{key}}", to_string(value)) 50 | end) 51 | end) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule LandingPage.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :landing_page, 7 | version: "0.0.1", 8 | elixir: "~> 1.4", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: [:phoenix, :gettext] ++ Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | aliases: aliases(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Configuration for the OTP application. 18 | # 19 | # Type `mix help compile.app` for more information. 20 | def application do 21 | [ 22 | mod: {LandingPage.Application, []}, 23 | extra_applications: [:logger, :runtime_tools] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [ 36 | {:phoenix, "~> 1.3.0"}, 37 | {:phoenix_pubsub, "~> 1.0"}, 38 | {:phoenix_ecto, "~> 3.2"}, 39 | {:postgrex, ">= 0.0.0"}, 40 | {:phoenix_html, "~> 2.10"}, 41 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 42 | {:gettext, "~> 0.11"}, 43 | {:cowboy, "~> 1.0"} 44 | ] 45 | end 46 | 47 | # Aliases are shortcuts or tasks specific to the current project. 48 | # For example, to create, migrate and run the seeds file at once: 49 | # 50 | # $ mix ecto.setup 51 | # 52 | # See the documentation for `Mix` for more info on aliases. 53 | defp aliases do 54 | [ 55 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 56 | "ecto.reset": ["ecto.drop", "ecto.setup"], 57 | test: ["ecto.create --quiet", "ecto.migrate", "test"] 58 | ] 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /assets/css/app.sass: -------------------------------------------------------------------------------- 1 | @charset "utf-8" 2 | 3 | // - - css-burrito v1.6 | mit license | github.com/jasonreece/css-burrito 4 | 5 | // - - - - - - - - - - - - - - - - - - - 6 | // - - index.scss 7 | // contains an import section for libs, global, and modules, the inbox, 8 | // and a shame section for quick fixes and hacks. 9 | 10 | @import sass/utilities/initial-variables 11 | 12 | // - - variable overrides for 3rd party libraries 13 | @import libs/libs-variable-overrides 14 | 15 | // - - - - - - - - - - - - - - - - - - - 16 | // - - libs 17 | // add 3rd party libraries here. 18 | // be sure to load them after the variable overrides file 19 | @import normalize 20 | @import bulma 21 | 22 | // - - - - - - - - - - - - - - - - - - - 23 | // - - global 24 | 25 | // - - global/settings 26 | // sass variables and maps 27 | @import global/settings 28 | 29 | // - - global/utilities 30 | // extends, functions, and mixins 31 | @import global/utilities 32 | 33 | // - - global/base 34 | // base-level tags (body, p, etc.) 35 | @import global/base 36 | 37 | // - - global/layout 38 | // margin, padding, sizing 39 | @import global/layout 40 | 41 | // - - global/skin 42 | // backgrounds, borders, box-shadow, etc 43 | @import global/skin 44 | 45 | // - - global/typography 46 | // fonts and colors 47 | @import global/typography 48 | 49 | // - - - - - - - - - - - - - - - - - - - 50 | // - - modules 51 | // add new modules to the modules/_modules.scss file and they'll get pulled in here. 52 | @import modules/modules 53 | 54 | // - - - - - - - - - - - - - - - - - - - 55 | // - - inbox 56 | // the inbox allows developers, and those not actively working on the project 57 | // to quickly add styles that are easily seen by the maintainer of the file. 58 | 59 | // - - - - - - - - - - - - - - - - - - - 60 | // - - shame 61 | // need to add a quick fix, hack, or questionable technique? add it here, fix it later. 62 | -------------------------------------------------------------------------------- /assets/elm/src/Update.elm: -------------------------------------------------------------------------------- 1 | module Update exposing (update) 2 | 3 | import Http exposing (Error(..)) 4 | import Json.Decode as Decode 5 | import Commands as Commands 6 | import Decoders exposing (validationErrorsDecoder) 7 | import Messages exposing (Msg(..)) 8 | import Model exposing (..) 9 | import Ports 10 | 11 | 12 | update : Msg -> Model -> ( Model, Cmd Msg ) 13 | update msg model = 14 | let 15 | subscribeForm = 16 | model.subscribeForm 17 | 18 | formFields = 19 | extractFormFields model.subscribeForm 20 | in 21 | case msg of 22 | HandleFullNameInput value -> 23 | { model | subscribeForm = Editing { formFields | fullName = value } } ! [] 24 | 25 | HandleEmailInput value -> 26 | { model | subscribeForm = Editing { formFields | email = value } } ! [] 27 | 28 | HandleFormSubmit -> 29 | { model | subscribeForm = Saving formFields } ! [ Commands.subscribe subscribeForm ] 30 | 31 | SubscribeResponse (Ok result) -> 32 | { model | subscribeForm = Success formFields } ! [] 33 | 34 | SubscribeResponse (Err (BadStatus response)) -> 35 | case Decode.decodeString validationErrorsDecoder response.body of 36 | Ok validationErrors -> 37 | { model | subscribeForm = Invalid formFields validationErrors } ! [ Ports.resetRecaptcha () ] 38 | 39 | Err error -> 40 | { model | subscribeForm = Invalid formFields emptyValidationErrors } ! [ Ports.resetRecaptcha () ] 41 | 42 | SubscribeResponse (Err error) -> 43 | { model | subscribeForm = Invalid formFields emptyValidationErrors } ! [ Ports.resetRecaptcha () ] 44 | 45 | SetRecaptchaToken token -> 46 | { model | subscribeForm = Editing { formFields | recaptchaToken = Just token } } ! [] 47 | -------------------------------------------------------------------------------- /lib/landing_page_web.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use LandingPageWeb, :controller 9 | use LandingPageWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: LandingPageWeb 23 | import Plug.Conn 24 | import LandingPageWeb.Router.Helpers 25 | import LandingPageWeb.Gettext 26 | 27 | action_fallback(LandingPageWeb.FallbackController) 28 | end 29 | end 30 | 31 | def view do 32 | quote do 33 | use Phoenix.View, 34 | root: "lib/landing_page_web/templates", 35 | namespace: LandingPageWeb 36 | 37 | # Import convenience functions from controllers 38 | import Phoenix.Controller, only: [get_flash: 2, view_module: 1] 39 | 40 | # Use all HTML functionality (forms, tags, etc) 41 | use Phoenix.HTML 42 | 43 | import LandingPageWeb.Router.Helpers 44 | import LandingPageWeb.ErrorHelpers 45 | import LandingPageWeb.Gettext 46 | end 47 | end 48 | 49 | def router do 50 | quote do 51 | use Phoenix.Router 52 | import Plug.Conn 53 | import Phoenix.Controller 54 | end 55 | end 56 | 57 | def channel do 58 | quote do 59 | use Phoenix.Channel 60 | import LandingPageWeb.Gettext 61 | end 62 | end 63 | 64 | @doc """ 65 | When used, dispatch to the appropriate controller/view/etc. 66 | """ 67 | defmacro __using__(which) when is_atom(which) do 68 | apply(__MODULE__, which, []) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/landing_page_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPageWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :landing_page 3 | 4 | socket("/socket", LandingPageWeb.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( 11 | Plug.Static, 12 | at: "/", 13 | from: :landing_page, 14 | gzip: false, 15 | only: ~w(css fonts images js favicon.ico robots.txt) 16 | ) 17 | 18 | # Code reloading can be explicitly enabled under the 19 | # :code_reloader configuration of your endpoint. 20 | if code_reloading? do 21 | socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket) 22 | plug(Phoenix.LiveReloader) 23 | plug(Phoenix.CodeReloader) 24 | end 25 | 26 | plug(Plug.RequestId) 27 | plug(Plug.Logger) 28 | 29 | plug( 30 | Plug.Parsers, 31 | parsers: [:urlencoded, :multipart, :json], 32 | pass: ["*/*"], 33 | json_decoder: Poison 34 | ) 35 | 36 | plug(Plug.MethodOverride) 37 | plug(Plug.Head) 38 | 39 | # The session will be stored in the cookie and signed, 40 | # this means its contents can be read but not tampered with. 41 | # Set :encryption_salt if you would also like to encrypt it. 42 | plug( 43 | Plug.Session, 44 | store: :cookie, 45 | key: "_landing_page_key", 46 | signing_salt: "hFM9Mh9C" 47 | ) 48 | 49 | plug(LandingPageWeb.Router) 50 | 51 | @doc """ 52 | Callback invoked for dynamically configuring the endpoint. 53 | 54 | It receives the endpoint configuration and checks if 55 | configuration should be loaded from the system environment. 56 | """ 57 | def init(_key, config) do 58 | if config[:load_from_system_env] do 59 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 60 | {:ok, Keyword.put(config, :http, [:inet6, port: port])} 61 | else 62 | {:ok, config} 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /assets/brunch-config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // See http://brunch.io/#documentation for docs. 3 | files: { 4 | javascripts: { 5 | joinTo: 'js/app.js', 6 | 7 | // To use a separate vendor.js bundle, specify two files path 8 | // http://brunch.io/docs/config#-files- 9 | // joinTo: { 10 | // "js/app.js": /^js/, 11 | // "js/vendor.js": /^(?!js)/ 12 | // } 13 | // 14 | // To change the order of concatenation of files, explicitly mention here 15 | // order: { 16 | // before: [ 17 | // "vendor/js/jquery-2.1.1.js", 18 | // "vendor/js/bootstrap.min.js" 19 | // ] 20 | // } 21 | }, 22 | stylesheets: { 23 | joinTo: 'css/app.css', 24 | }, 25 | templates: { 26 | joinTo: 'js/app.js', 27 | }, 28 | }, 29 | 30 | conventions: { 31 | // This option sets where we should place non-css and non-js assets in. 32 | // By default, we set this to "/assets/static". Files in this directory 33 | // will be copied to `paths.public`, which is "priv/static" by default. 34 | assets: /^(static)/, 35 | }, 36 | 37 | // Phoenix paths configuration 38 | paths: { 39 | // Dependencies and current project directories to watch 40 | watched: ['static', 'css', 'js', 'vendor', 'elm'], 41 | // Where to compile files to 42 | public: '../priv/static', 43 | }, 44 | 45 | // Configure your plugins 46 | plugins: { 47 | babel: { 48 | // Do not use ES6 compiler in vendor code 49 | ignore: [/vendor/], 50 | }, 51 | elmBrunch: { 52 | mainModules: ['src/Main.elm'], 53 | elmFolder: 'elm', 54 | outputFolder: '../js/elm', 55 | makeParameters: ['--warn', '--debug'], 56 | }, 57 | sass: { 58 | mode: 'native', 59 | sourceMapEmbed: true, 60 | options: { 61 | includePaths: [ 62 | 'node_modules/normalize-scss/sass/', 63 | 'node_modules/bulma/', 64 | ], 65 | }, 66 | }, 67 | }, 68 | 69 | modules: { 70 | autoRequire: { 71 | 'js/app.js': ['js/app'], 72 | }, 73 | }, 74 | 75 | npm: { 76 | enabled: true, 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :landing_page, LandingPageWeb.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [ 15 | node: [ 16 | "node_modules/brunch/bin/brunch", 17 | "watch", 18 | "--stdin", 19 | cd: Path.expand("../assets", __DIR__) 20 | ] 21 | ] 22 | 23 | # ## SSL Support 24 | # 25 | # In order to use HTTPS in development, a self-signed 26 | # certificate can be generated by running the following 27 | # command from your terminal: 28 | # 29 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem 30 | # 31 | # The `http:` config above can be replaced with: 32 | # 33 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"], 34 | # 35 | # If desired, both `http:` and `https:` keys can be 36 | # configured to run both http and https servers on 37 | # different ports. 38 | 39 | # Watch static and templates for browser reloading. 40 | config :landing_page, LandingPageWeb.Endpoint, 41 | live_reload: [ 42 | patterns: [ 43 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 44 | ~r{priv/gettext/.*(po)$}, 45 | ~r{lib/landing_page_web/views/.*(ex)$}, 46 | ~r{lib/landing_page_web/templates/.*(eex)$} 47 | ] 48 | ] 49 | 50 | # Do not include metadata nor timestamps in development logs 51 | config :logger, :console, format: "[$level] $message\n" 52 | 53 | # Set a higher stacktrace during development. Avoid configuring such 54 | # in production as building large stacktraces may be expensive. 55 | config :phoenix, :stacktrace_depth, 20 56 | 57 | # Configure your database 58 | config :landing_page, LandingPage.Repo, 59 | adapter: Ecto.Adapters.Postgres, 60 | username: "postgres", 61 | password: "postgres", 62 | database: "landing_page_dev", 63 | hostname: "localhost", 64 | pool_size: 10 65 | -------------------------------------------------------------------------------- /lib/landing_page/marketing/marketing.ex: -------------------------------------------------------------------------------- 1 | defmodule LandingPage.Marketing do 2 | @moduledoc """ 3 | The Marketing context. 4 | """ 5 | 6 | import Ecto.Query, warn: false 7 | alias LandingPage.Repo 8 | 9 | alias LandingPage.Marketing.Lead 10 | 11 | @doc """ 12 | Returns the list of leads. 13 | 14 | ## Examples 15 | 16 | iex> list_leads() 17 | [%Lead{}, ...] 18 | 19 | """ 20 | def list_leads do 21 | Repo.all(Lead) 22 | end 23 | 24 | @doc """ 25 | Gets a single lead. 26 | 27 | Raises `Ecto.NoResultsError` if the Lead does not exist. 28 | 29 | ## Examples 30 | 31 | iex> get_lead!(123) 32 | %Lead{} 33 | 34 | iex> get_lead!(456) 35 | ** (Ecto.NoResultsError) 36 | 37 | """ 38 | def get_lead!(id), do: Repo.get!(Lead, id) 39 | 40 | @doc """ 41 | Creates a lead. 42 | 43 | ## Examples 44 | 45 | iex> create_lead(%{field: value}) 46 | {:ok, %Lead{}} 47 | 48 | iex> create_lead(%{field: bad_value}) 49 | {:error, %Ecto.Changeset{}} 50 | 51 | """ 52 | def create_lead(attrs \\ %{}) do 53 | %Lead{} 54 | |> Lead.changeset(attrs) 55 | |> Repo.insert() 56 | end 57 | 58 | @doc """ 59 | Updates a lead. 60 | 61 | ## Examples 62 | 63 | iex> update_lead(lead, %{field: new_value}) 64 | {:ok, %Lead{}} 65 | 66 | iex> update_lead(lead, %{field: bad_value}) 67 | {:error, %Ecto.Changeset{}} 68 | 69 | """ 70 | def update_lead(%Lead{} = lead, attrs) do 71 | lead 72 | |> Lead.changeset(attrs) 73 | |> Repo.update() 74 | end 75 | 76 | @doc """ 77 | Deletes a Lead. 78 | 79 | ## Examples 80 | 81 | iex> delete_lead(lead) 82 | {:ok, %Lead{}} 83 | 84 | iex> delete_lead(lead) 85 | {:error, %Ecto.Changeset{}} 86 | 87 | """ 88 | def delete_lead(%Lead{} = lead) do 89 | Repo.delete(lead) 90 | end 91 | 92 | @doc """ 93 | Returns an `%Ecto.Changeset{}` for tracking lead changes. 94 | 95 | ## Examples 96 | 97 | iex> change_lead(lead) 98 | %Ecto.Changeset{source: %Lead{}} 99 | 100 | """ 101 | def change_lead(%Lead{} = lead) do 102 | Lead.changeset(lead, %{}) 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we often load configuration from external 4 | # sources, such as your system environment. For this reason, 5 | # you won't find the :http configuration below, but set inside 6 | # LandingPageWeb.Endpoint.init/2 when load_from_system_env is 7 | # true. Any dynamic configuration should be done there. 8 | # 9 | # Don't forget to configure the url host to something meaningful, 10 | # Phoenix uses this information when generating URLs. 11 | # 12 | # Finally, we also include the path to a cache manifest 13 | # containing the digested version of static files. This 14 | # manifest is generated by the mix phx.digest task 15 | # which you typically run after static files are built. 16 | config :landing_page, LandingPageWeb.Endpoint, 17 | load_from_system_env: true, 18 | url: [host: "example.com", port: 80], 19 | cache_static_manifest: "priv/static/cache_manifest.json" 20 | 21 | # Do not print debug messages in production 22 | config :logger, level: :info 23 | 24 | # ## SSL Support 25 | # 26 | # To get SSL working, you will need to add the `https` key 27 | # to the previous section and set your `:url` port to 443: 28 | # 29 | # config :landing_page, LandingPageWeb.Endpoint, 30 | # ... 31 | # url: [host: "example.com", port: 443], 32 | # https: [:inet6, 33 | # port: 443, 34 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 35 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 36 | # 37 | # Where those two env variables return an absolute path to 38 | # the key and cert in disk or a relative path inside priv, 39 | # for example "priv/ssl/server.key". 40 | # 41 | # We also recommend setting `force_ssl`, ensuring no data is 42 | # ever sent via http, always redirecting to https: 43 | # 44 | # config :landing_page, LandingPageWeb.Endpoint, 45 | # force_ssl: [hsts: true] 46 | # 47 | # Check `Plug.SSL` for all available options in `force_ssl`. 48 | 49 | # ## Using releases 50 | # 51 | # If you are doing OTP releases, you need to instruct Phoenix 52 | # to start the server for all endpoints: 53 | # 54 | # config :phoenix, :serve_endpoints, true 55 | # 56 | # Alternatively, you can configure exactly which server to 57 | # start per endpoint: 58 | # 59 | # config :landing_page, LandingPageWeb.Endpoint, server: true 60 | # 61 | 62 | # Finally import the config/prod.secret.exs 63 | # which should be versioned separately. 64 | import_config "prod.secret.exs" 65 | -------------------------------------------------------------------------------- /test/landing_page/marketing/marketing_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LandingPage.MarketingTest do 2 | use LandingPage.DataCase 3 | 4 | alias LandingPage.Marketing 5 | 6 | describe "leads" do 7 | alias LandingPage.Marketing.Lead 8 | 9 | @valid_attrs %{email: "some email", full_name: "some full_name", recaptcha_token: "foo"} 10 | @update_attrs %{ 11 | email: "some updated email", 12 | full_name: "some updated full_name", 13 | recaptcha_token: "foo" 14 | } 15 | @invalid_attrs %{email: nil, full_name: nil, recaptcha_token: nil} 16 | 17 | def lead_fixture(attrs \\ %{}) do 18 | {:ok, lead} = 19 | attrs 20 | |> Enum.into(@valid_attrs) 21 | |> Marketing.create_lead() 22 | 23 | %{lead | recaptcha_token: nil} 24 | end 25 | 26 | test "list_leads/0 returns all leads" do 27 | lead = lead_fixture() 28 | assert Marketing.list_leads() == [lead] 29 | end 30 | 31 | test "get_lead!/1 returns the lead with given id" do 32 | lead = lead_fixture() 33 | assert Marketing.get_lead!(lead.id) == lead 34 | end 35 | 36 | test "create_lead/1 with valid data creates a lead" do 37 | assert {:ok, %Lead{} = lead} = Marketing.create_lead(@valid_attrs) 38 | assert lead.email == "some email" 39 | assert lead.full_name == "some full_name" 40 | end 41 | 42 | test "create_lead/1 with invalid data returns error changeset" do 43 | assert {:error, %Ecto.Changeset{}} = Marketing.create_lead(@invalid_attrs) 44 | end 45 | 46 | test "update_lead/2 with valid data updates the lead" do 47 | lead = lead_fixture() 48 | assert {:ok, lead} = Marketing.update_lead(lead, @update_attrs) 49 | assert %Lead{} = lead 50 | assert lead.email == "some updated email" 51 | assert lead.full_name == "some updated full_name" 52 | end 53 | 54 | test "update_lead/2 with invalid data returns error changeset" do 55 | lead = lead_fixture() 56 | assert {:error, %Ecto.Changeset{}} = Marketing.update_lead(lead, @invalid_attrs) 57 | assert lead == Marketing.get_lead!(lead.id) 58 | end 59 | 60 | test "delete_lead/1 deletes the lead" do 61 | lead = lead_fixture() 62 | assert {:ok, %Lead{}} = Marketing.delete_lead(lead) 63 | assert_raise Ecto.NoResultsError, fn -> Marketing.get_lead!(lead.id) end 64 | end 65 | 66 | test "change_lead/1 returns a lead changeset" do 67 | lead = lead_fixture() 68 | assert %Ecto.Changeset{} = Marketing.change_lead(lead) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_acceptance/3 26 | msgid "must be accepted" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_format/3 30 | msgid "has invalid format" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_subset/3 34 | msgid "has an invalid entry" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_exclusion/3 38 | msgid "is reserved" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.validate_confirmation/3 42 | msgid "does not match confirmation" 43 | msgstr "" 44 | 45 | ## From Ecto.Changeset.no_assoc_constraint/3 46 | msgid "is still associated with this entry" 47 | msgstr "" 48 | 49 | msgid "are still associated with this entry" 50 | msgstr "" 51 | 52 | ## From Ecto.Changeset.validate_length/3 53 | msgid "should be %{count} character(s)" 54 | msgid_plural "should be %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have %{count} item(s)" 59 | msgid_plural "should have %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at least %{count} character(s)" 64 | msgid_plural "should be at least %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at least %{count} item(s)" 69 | msgid_plural "should have at least %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "should be at most %{count} character(s)" 74 | msgid_plural "should be at most %{count} character(s)" 75 | msgstr[0] "" 76 | msgstr[1] "" 77 | 78 | msgid "should have at most %{count} item(s)" 79 | msgid_plural "should have at most %{count} item(s)" 80 | msgstr[0] "" 81 | msgstr[1] "" 82 | 83 | ## From Ecto.Changeset.validate_number/3 84 | msgid "must be less than %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than %{number}" 88 | msgstr "" 89 | 90 | msgid "must be less than or equal to %{number}" 91 | msgstr "" 92 | 93 | msgid "must be greater than or equal to %{number}" 94 | msgstr "" 95 | 96 | msgid "must be equal to %{number}" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_acceptance/3 24 | msgid "must be accepted" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_format/3 28 | msgid "has invalid format" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_subset/3 32 | msgid "has an invalid entry" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_exclusion/3 36 | msgid "is reserved" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.validate_confirmation/3 40 | msgid "does not match confirmation" 41 | msgstr "" 42 | 43 | ## From Ecto.Changeset.no_assoc_constraint/3 44 | msgid "is still associated with this entry" 45 | msgstr "" 46 | 47 | msgid "are still associated with this entry" 48 | msgstr "" 49 | 50 | ## From Ecto.Changeset.validate_length/3 51 | msgid "should be %{count} character(s)" 52 | msgid_plural "should be %{count} character(s)" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | msgid "should have %{count} item(s)" 57 | msgid_plural "should have %{count} item(s)" 58 | msgstr[0] "" 59 | msgstr[1] "" 60 | 61 | msgid "should be at least %{count} character(s)" 62 | msgid_plural "should be at least %{count} character(s)" 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | msgid "should have at least %{count} item(s)" 67 | msgid_plural "should have at least %{count} item(s)" 68 | msgstr[0] "" 69 | msgstr[1] "" 70 | 71 | msgid "should be at most %{count} character(s)" 72 | msgid_plural "should be at most %{count} character(s)" 73 | msgstr[0] "" 74 | msgstr[1] "" 75 | 76 | msgid "should have at most %{count} item(s)" 77 | msgid_plural "should have at most %{count} item(s)" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | ## From Ecto.Changeset.validate_number/3 82 | msgid "must be less than %{number}" 83 | msgstr "" 84 | 85 | msgid "must be greater than %{number}" 86 | msgstr "" 87 | 88 | msgid "must be less than or equal to %{number}" 89 | msgstr "" 90 | 91 | msgid "must be greater than or equal to %{number}" 92 | msgstr "" 93 | 94 | msgid "must be equal to %{number}" 95 | msgstr "" 96 | -------------------------------------------------------------------------------- /assets/css/modules/pages/_landing.sass: -------------------------------------------------------------------------------- 1 | .landing-page 2 | .main-wrapper 3 | +desktop 4 | display: flex 5 | height: 100vh 6 | 7 | .left 8 | align-items: center 9 | background: $purple-dark 10 | position: relative 11 | text-align: center 12 | padding: 4rem 1rem 13 | 14 | +mobile 15 | height: 100vh 16 | padding: 0 17 | display: flex 18 | flex-direction: column 19 | 20 | +desktop 21 | display: flex 22 | flex: 1 23 | padding: 4rem 24 | 25 | .hero 26 | width: 100% 27 | 28 | +mobile 29 | padding: 0 1em 30 | flex: 1 31 | display: flex 32 | justify-content: center 33 | 34 | .header 35 | width: 100px 36 | display: block 37 | margin: 0 auto 38 | 39 | +mobile 40 | height: 6rem 41 | display: flex 42 | justify-content: center 43 | align-items: center 44 | 45 | +desktop 46 | position: absolute 47 | width: 100px 48 | top: 4rem 49 | 50 | .scroll-to 51 | height: 5rem 52 | .fa 53 | color: $white 54 | font-size: $size-2 55 | 56 | +desktop 57 | display: none 58 | 59 | .title 60 | color: $white 61 | text-transform: uppercase 62 | font-size: $size-2 63 | 64 | +mobile 65 | margin: 0 0 1.5em 66 | 67 | +desktop 68 | line-height: 2em 69 | 70 | .subtitle 71 | color: rgba($white, .7) 72 | 73 | +desktop 74 | font-size: $size-4 75 | line-height: 1.5em 76 | 77 | strong 78 | color: $white 79 | 80 | .right 81 | align-items: center 82 | padding: 1rem 83 | padding: 1rem 84 | 85 | #recaptcha 86 | > iframe 87 | width: 100% 88 | 89 | +mobile 90 | button 91 | width: 100% 92 | 93 | +desktop 94 | display: flex 95 | flex: 1 96 | padding: 4rem 97 | 98 | form 99 | width: 70% 100 | 101 | .control 102 | margin-bottom: 1em 103 | 104 | .select, select 105 | width: 100% 106 | 107 | .section 108 | width: 100% 109 | 110 | .success-message 111 | text-align: center 112 | 113 | +mobile 114 | padding: 3rem 0 115 | 116 | .fa 117 | color: $red 118 | font-size: 5em 119 | margin-bottom: 1em 120 | 121 | h2 122 | font-size: $size-4 123 | margin-bottom: 1em 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [], [], "hexpm"}, 3 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [], [], "hexpm"}, 5 | "db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 6 | "decimal": {:hex, :decimal, "1.4.1", "ad9e501edf7322f122f7fc151cce7c2a0c9ada96f2b0155b8a09a795c2029770", [], [], "hexpm"}, 7 | "ecto": {:hex, :ecto, "2.2.6", "3fd1067661d6d64851a0d4db9acd9e884c00d2d1aa41cc09da687226cf894661", [], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 8 | "file_system": {:hex, :file_system, "0.2.2", "7f1e9de4746f4eb8a4ca8f2fbab582d84a4e40fa394cce7bfcb068b988625b06", [], [], "hexpm"}, 9 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [], [], "hexpm"}, 10 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [], [], "hexpm"}, 11 | "phoenix": {:hex, :phoenix, "1.3.0", "1c01124caa1b4a7af46f2050ff11b267baa3edb441b45dbf243e979cd4c5891b", [], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.3.0", "702f6e164512853d29f9d20763493f2b3bcfcb44f118af2bc37bb95d0801b480", [], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "phoenix_html": {:hex, :phoenix_html, "2.10.5", "4f9df6b0fb7422a9440a73182a566cb9cbe0e3ffe8884ef9337ccf284fc1ef0a", [], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.1.3", "1d178429fc8950b12457d09c6afec247bfe1fcb6f36209e18fbb0221bdfe4d41", [], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"}, 15 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [], [], "hexpm"}, 16 | "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, 17 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [], [], "hexpm"}, 18 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [], [], "hexpm"}, 19 | "postgrex": {:hex, :postgrex, "0.13.3", "c277cfb2a9c5034d445a722494c13359e361d344ef6f25d604c2353185682bfc", [], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, 20 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [], [], "hexpm"}, 21 | } 22 | -------------------------------------------------------------------------------- /assets/elm/src/View.elm: -------------------------------------------------------------------------------- 1 | module View exposing (view) 2 | 3 | import Dict exposing (Dict) 4 | import Html exposing (Html, form) 5 | import Html.Attributes as Html 6 | import Html.Events as Html 7 | import Messages exposing (Msg(..)) 8 | import Model exposing (..) 9 | 10 | 11 | view : Model -> Html Msg 12 | view { subscribeForm } = 13 | case subscribeForm of 14 | Success _ -> 15 | Html.div 16 | [ Html.class "success-message" ] 17 | [ Html.div 18 | [ Html.class "icon is-large" ] 19 | [ Html.i 20 | [ Html.class "fa fa-3x fa-heart" ] 21 | [] 22 | ] 23 | , Html.h2 24 | [] 25 | [ Html.text "You have subscribed with success" ] 26 | , Html.p 27 | [] 28 | [ Html.text "We will keep you updated with the latest news" ] 29 | ] 30 | 31 | _ -> 32 | formView subscribeForm 33 | 34 | 35 | formView : SubscribeForm -> Html Msg 36 | formView subscribeForm = 37 | let 38 | { fullName, email, recaptchaToken } = 39 | extractFormFields subscribeForm 40 | 41 | validationErrors = 42 | extractValidationErrors subscribeForm 43 | 44 | saving = 45 | case subscribeForm of 46 | Saving _ -> 47 | True 48 | 49 | _ -> 50 | False 51 | 52 | invalid = 53 | case subscribeForm of 54 | Invalid _ _ -> 55 | True 56 | 57 | _ -> 58 | False 59 | 60 | buttonDisabled = 61 | fullName 62 | == "" 63 | || email 64 | == "" 65 | || recaptchaToken 66 | == Nothing 67 | || recaptchaToken 68 | == Just "" 69 | || saving 70 | || invalid 71 | in 72 | Html.div 73 | [ Html.class "content" ] 74 | [ Html.h3 75 | [] 76 | [ Html.text "Want to know more?" ] 77 | , Html.p 78 | [] 79 | [ Html.text "Subscribe to stay updated" ] 80 | , form 81 | [ Html.onSubmit HandleFormSubmit ] 82 | [ Html.div 83 | [ Html.class "field" ] 84 | [ Html.div 85 | [ Html.class "control" ] 86 | [ Html.input 87 | [ Html.classList 88 | [ ( "input is-medium", True ) 89 | , ( "is-danger", Dict.member "full_name" validationErrors ) 90 | ] 91 | , Html.placeholder "My name is..." 92 | , Html.required True 93 | , Html.value fullName 94 | , Html.onInput HandleFullNameInput 95 | ] 96 | [] 97 | , validationErrorView "full_name" validationErrors 98 | ] 99 | ] 100 | , Html.div 101 | [ Html.class "field" ] 102 | [ Html.div 103 | [ Html.class "control" ] 104 | [ Html.input 105 | [ Html.classList 106 | [ ( "input is-medium", True ) 107 | , ( "is-danger", Dict.member "email" validationErrors ) 108 | ] 109 | , Html.type_ "email" 110 | , Html.placeholder "My email address is..." 111 | , Html.required True 112 | , Html.value email 113 | , Html.onInput HandleEmailInput 114 | ] 115 | [] 116 | , validationErrorView "email" validationErrors 117 | ] 118 | ] 119 | , Html.div 120 | [ Html.class "field" ] 121 | [ Html.div 122 | [ Html.id "recaptcha" ] 123 | [] 124 | , validationErrorView "recaptcha_token" validationErrors 125 | ] 126 | , Html.div 127 | [ Html.class "field" ] 128 | [ Html.div 129 | [ Html.class "control" ] 130 | [ Html.button 131 | [ Html.class "button is-primary is-medium" 132 | , Html.disabled buttonDisabled 133 | ] 134 | [ Html.span 135 | [ Html.class "icon" ] 136 | [ Html.i 137 | [ Html.classList 138 | [ ( "fa fa-check", not saving ) 139 | , ( "fa fa-circle-o-notch fa-spin", saving ) 140 | ] 141 | ] 142 | [] 143 | ] 144 | , Html.span 145 | [] 146 | [ Html.text "Subscribe me" ] 147 | ] 148 | ] 149 | ] 150 | ] 151 | ] 152 | 153 | 154 | validationErrorView : String -> ValidationErrors -> Html Msg 155 | validationErrorView key validationErrors = 156 | case Dict.get key validationErrors of 157 | Just error -> 158 | error 159 | |> List.map Html.text 160 | |> Html.p 161 | [ Html.class "help is-danger" ] 162 | 163 | Nothing -> 164 | Html.text "" 165 | --------------------------------------------------------------------------------