~electric/shop-oregon-local

34ba4245198533bc889faa34f502872c96247896 — Micah D 5 months ago
Initial work
44 files changed, 1374 insertions(+), 0 deletions(-)

A .formatter.exs
A .gitignore
A README.md
A config/config.exs
A config/dev.exs
A config/prod.exs
A config/prod.secret.exs
A config/test.exs
A lib/shop_local.ex
A lib/shop_local/application.ex
A lib/shop_local/bursts_chocolates_search_provider.ex
A lib/shop_local/clothing_tree_search_provider.ex
A lib/shop_local/combined_search.ex
A lib/shop_local/conundrum_house_search_provider.ex
A lib/shop_local/oregon_coffee_and_tea_search_provider.ex
A lib/shop_local/pegasus_search_provider.ex
A lib/shop_local/restyle_corvallis_search_provider.ex
A lib/shop_local/robnetts_search_provider.ex
A lib/shop_local/toy_factory_search_provider.ex
A lib/shop_local_web.ex
A lib/shop_local_web/channels/user_socket.ex
A lib/shop_local_web/controllers/page_controller.ex
A lib/shop_local_web/endpoint.ex
A lib/shop_local_web/gettext.ex
A lib/shop_local_web/router.ex
A lib/shop_local_web/telemetry.ex
A lib/shop_local_web/templates/layout/app.html.eex
A lib/shop_local_web/templates/page/index.html.eex
A lib/shop_local_web/views/error_helpers.ex
A lib/shop_local_web/views/error_view.ex
A lib/shop_local_web/views/layout_view.ex
A lib/shop_local_web/views/page_view.ex
A mix.exs
A mix.lock
A priv/gettext/en/LC_MESSAGES/errors.po
A priv/gettext/errors.pot
A run_prod.sh
A test/shop_local_web/controllers/page_controller_test.exs
A test/shop_local_web/views/error_view_test.exs
A test/shop_local_web/views/layout_view_test.exs
A test/shop_local_web/views/page_view_test.exs
A test/support/channel_case.ex
A test/support/conn_case.ex
A test/test_helper.exs
A  => .formatter.exs +4 -0
@@ 1,4 @@
[
  import_deps: [:phoenix],
  inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"]
]

A  => .gitignore +28 -0
@@ 1,28 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
shop_local-*.tar

# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/

A  => README.md +18 -0
@@ 1,18 @@
# ShopLocal

To start your Phoenix server:

  * Install dependencies with `mix deps.get`
  * Start Phoenix endpoint with `mix phx.server`

Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.

Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).

## Learn more

  * Official website: https://www.phoenixframework.org/
  * Guides: https://hexdocs.pm/phoenix/overview.html
  * Docs: https://hexdocs.pm/phoenix
  * Forum: https://elixirforum.com/c/phoenix-forum
  * Source: https://github.com/phoenixframework/phoenix

A  => config/config.exs +30 -0
@@ 1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.

# General application configuration
use Mix.Config

# Configures the endpoint
config :shop_local, ShopLocalWeb.Endpoint,
  url: [host: "localhost"],
  secret_key_base: "WghJa6o2ShlCpilE3T745KWP7bb21D52TPKTiQjNQVEvhPPqV0Cjq8PXlwsTYzY0",
  render_errors: [view: ShopLocalWeb.ErrorView, accepts: ~w(html json), layout: false],
  pubsub_server: ShopLocal.PubSub,
  live_view: [signing_salt: "1rQevy+V"]

# Configures Elixir's Logger
config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id]

# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

config :shop_local, :basic_auth, username: "someone", password: "something"

A  => config/dev.exs +59 -0
@@ 1,59 @@
use Mix.Config

# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with webpack to recompile .js and .css sources.
config :shop_local, ShopLocalWeb.Endpoint,
  http: [port: 4000],
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  watchers: []

# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
# certificate can be generated by running the following
# Mix task:
#
#     mix phx.gen.cert
#
# Note that this task requires Erlang/OTP 20 or later.
# Run `mix help phx.gen.cert` for more information.
#
# The `http:` config above can be replaced with:
#
#     https: [
#       port: 4001,
#       cipher_suite: :strong,
#       keyfile: "priv/cert/selfsigned_key.pem",
#       certfile: "priv/cert/selfsigned.pem"
#     ],
#
# If desired, both `http:` and `https:` keys can be
# configured to run both http and https servers on
# different ports.

# Watch static and templates for browser reloading.
config :shop_local, ShopLocalWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
      ~r"priv/gettext/.*(po)$",
      ~r"lib/shop_local_web/(live|views)/.*(ex)$",
      ~r"lib/shop_local_web/templates/.*(eex)$"
    ]
  ]

# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"

# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20

# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime

A  => config/prod.exs +55 -0
@@ 1,55 @@
use Mix.Config

# For production, don't forget to configure the url host
# to something meaningful, Phoenix uses this information
# when generating URLs.
#
# Note we also include the path to a cache manifest
# containing the digested version of static files. This
# manifest is generated by the `mix phx.digest` task,
# which you should run after static files are built and
# before starting your production server.
config :shop_local, ShopLocalWeb.Endpoint,
  url: [host: "example.com", port: 80],
  cache_static_manifest: "priv/static/cache_manifest.json"

# Do not print debug messages in production
config :logger, level: :info

# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
# to the previous section and set your `:url` port to 443:
#
#     config :shop_local, ShopLocalWeb.Endpoint,
#       ...
#       url: [host: "example.com", port: 443],
#       https: [
#         port: 443,
#         cipher_suite: :strong,
#         keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
#         certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
#         transport_options: [socket_opts: [:inet6]]
#       ]
#
# The `cipher_suite` is set to `:strong` to support only the
# latest and more secure SSL ciphers. This means old browsers
# and clients may not be supported. You can set it to
# `:compatible` for wider support.
#
# `:keyfile` and `:certfile` expect an absolute path to the key
# and cert in disk or a relative path inside priv, for example
# "priv/ssl/server.key". For all supported SSL configuration
# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
#
# We also recommend setting `force_ssl` in your endpoint, ensuring
# no data is ever sent via http, always redirecting to https:
#
#     config :shop_local, ShopLocalWeb.Endpoint,
#       force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.

# Finally import the config/prod.secret.exs which loads secrets
# and configuration from environment variables.
import_config "prod.secret.exs"

A  => config/prod.secret.exs +30 -0
@@ 1,30 @@
# In this file, we load production configuration and secrets
# from environment variables. You can also hardcode secrets,
# although such is generally not recommended and you have to
# remember to add this file to your .gitignore.
use Mix.Config

secret_key_base =
  System.get_env("SECRET_KEY_BASE") ||
    raise """
    environment variable SECRET_KEY_BASE is missing.
    You can generate one by calling: mix phx.gen.secret
    """

config :shop_local, ShopLocalWeb.Endpoint,
  http: [
    port: String.to_integer(System.get_env("PORT") || "4000"),
    transport_options: [socket_opts: [:inet6]]
  ],
  secret_key_base: secret_key_base

config :shop_local, :basic_auth, username: System.get_env("DASH_USER"), password: System.get_env("DASH_PASS")
# ## Using releases (Elixir v1.9+)
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start each relevant endpoint:
#
#     config :shop_local, ShopLocalWeb.Endpoint, server: true
#
# Then you can assemble a release by calling `mix release`.
# See `mix help release` for more information.

A  => config/test.exs +10 -0
@@ 1,10 @@
use Mix.Config

# We don't run a server during test. If one is required,
# you can enable the server option below.
config :shop_local, ShopLocalWeb.Endpoint,
  http: [port: 4002],
  server: false

# Print only warnings and errors during test
config :logger, level: :warn

A  => lib/shop_local.ex +9 -0
@@ 1,9 @@
defmodule ShopLocal do
  @moduledoc """
  ShopLocal keeps the contexts that define your domain
  and business logic.

  Contexts are also responsible for managing your data, regardless
  if it comes from the database, an external API or others.
  """
end

A  => lib/shop_local/application.ex +32 -0
@@ 1,32 @@
defmodule ShopLocal.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      # Start the Telemetry supervisor
      ShopLocalWeb.Telemetry,
      # Start the PubSub system
      {Phoenix.PubSub, name: ShopLocal.PubSub},
      # Start the Endpoint (http/https)
      ShopLocalWeb.Endpoint
      # Start a worker by calling: ShopLocal.Worker.start_link(arg)
      # {ShopLocal.Worker, arg}
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: ShopLocal.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # Tell Phoenix to update the endpoint configuration
  # whenever the application is updated.
  def config_change(changed, _new, removed) do
    ShopLocalWeb.Endpoint.config_change(changed, removed)
    :ok
  end
end

A  => lib/shop_local/bursts_chocolates_search_provider.ex +51 -0
@@ 1,51 @@
defmodule ShopLocal.BurstsChocolatesSearchProvider do

  def search(s) do
    case HTTPoison.get("https://burstschocolates.com/?target=search&mode=search&substring=#{s}") do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        {:ok, document} = Floki.parse_document(body)
        results = document
                  |> Floki.find(".product")
                  |> Enum.map(fn (k) -> 
                    name = k
                           |> Floki.find(".product-name")
                           |> Floki.text
                    link = k
                           |> Floki.find(".product-thumbnail") 
                           |> Floki.attribute("href")
                           |> List.last

                    image_src = k
                                |> Floki.find(".product-thumbnail img")
                                |> Floki.attribute("src")

                    price = k
                      |> Floki.find("span.price.product-price")
                      |> Floki.text

                    link = case String.starts_with?(link, "http") do
                      false -> "https://burstschocolates.com" <> link
                      true ->  link
                    end

                      %{
                        provider: "Bursts Chocolates",
                        name: name,
                        desc: "",
                        price: price,
                        link: link,
                        image_src: image_src
                      } 
                  end)
                  |> Enum.filter(fn 
                    %{ name: "" } -> false
                    _ -> true
                  end)
        {:ok, results}
        {:ok, %HTTPoison.Response{status_code: 404}} ->
        {:error, "Not found." }
        {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, reason}
    end
  end
end

A  => lib/shop_local/clothing_tree_search_provider.ex +45 -0
@@ 1,45 @@
defmodule ShopLocal.ClothingTreeSearchProvider do

  def search(s) do
    case HTTPoison.get("https://www.clothes-tree.com/search?q=#{s}") do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        {:ok, document} = Floki.parse_document(body)
        results = document
                  |> Floki.find(".main-content .product-item")
                  |> Enum.map(fn (k) -> 
                    name = k
                           |> Floki.find(".product-item__title") 
                           |> Floki.text
                    link = k
                           |> Floki.find("a.product-item__link") 
                           |> Floki.attribute("href")
                    image_src = k
                                |> Floki.find("img.product-item__image")
                                |> Floki.attribute("src")

                    price = case Floki.find(k, ".product-item__meta__inner p:last-child") do
                      [{_tag, _attrs, price_children}] -> List.last(price_children)
                      _ -> "$XXX"
                    end

                      %{
                        provider: "Clothing Tree",
                        name: name,
                        desc: "",
                        price: price,
                        link: link,
                        image_src: image_src
                      } 
                  end)
                  |> Enum.filter(fn 
                    %{ name: "" } -> false
                    _ -> true
                  end)
        {:ok, results}
        {:ok, %HTTPoison.Response{status_code: 404}} ->
        {:error, "Not found." }
        {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, reason}
    end
  end
end

A  => lib/shop_local/combined_search.ex +53 -0
@@ 1,53 @@
defmodule ShopLocal.CombinedSearch do
  alias ShopLocal.PegasusSearchProvider
  alias ShopLocal.ToyFactorySearchProvider
  alias ShopLocal.ClothingTreeSearchProvider
  alias ShopLocal.BurstsChocolatesSearchProvider
  alias ShopLocal.OregonCoffeeAndTeaSearchProvider
  alias ShopLocal.ConundrumHouseSearchProvider
  alias ShopLocal.RobnettsSearchProvider
  alias ShopLocal.RestyleCorvallisSearchProvider

  require Logger

  def listen(0, results) do
    results
  end
  def listen(count, results) do
    receive do
      {:ok, res} -> 
        results = Enum.concat(results, res)
        listen(count - 1, results)
      {:error, message} ->
        Logger.warn(message)
        listen(count - 1, results)
    after
      10_000 ->
        Logger.warn("10s timeout")
        results
    end
  end

  @providers [
    ToyFactorySearchProvider,
    PegasusSearchProvider,
    ClothingTreeSearchProvider,
    BurstsChocolatesSearchProvider,
    OregonCoffeeAndTeaSearchProvider,
    ConundrumHouseSearchProvider,
    RobnettsSearchProvider,
    RestyleCorvallisSearchProvider
  ]
  @provider_len Enum.count(@providers)

  def search(s) do
    parent = self()
    tasks = Enum.map(@providers, fn(k) -> 
      Task.start(fn -> send(parent, k.search(s)) end)
    end)
    results = listen(@provider_len, [])
              |> Enum.map(fn(k) -> Map.put(k, :desc, String.slice(k.desc, 0, 400)) end)
              |> Enum.shuffle
    {:ok, results}
  end
end

A  => lib/shop_local/conundrum_house_search_provider.ex +52 -0
@@ 1,52 @@
defmodule ShopLocal.ConundrumHouseSearchProvider do

  def search(s) do
    case HTTPoison.get("https://conundrum.house/search?q=#{s}") do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        {:ok, document} = Floki.parse_document(body)
        results = document
                  |> Floki.find(".list-view-item .product-card")
                  |> Enum.map(fn (k) -> 
                    name = k
                           |> Floki.find(".product-card__title")
                           |> Floki.text
                    link = k
                           |> Floki.find("a") 
                           |> Floki.attribute("href")
                           |> List.first

                    link = case String.starts_with?(link, "http") do
                      false -> "https://conundrum.house" <> link
                      true ->  link
                    end
                    IO.puts link

                    image_src = k
                                |> Floki.find("img.list-view-item__image")
                                |> Floki.attribute("src")

                    price = k
                      |> Floki.find(".price-item.price-item--regular")
                      |> Floki.text

                    %{
                      provider: "Conundrum House",
                      name: name,
                      desc: "",
                      price: price,
                      link: link,
                      image_src: image_src
                    } 
                  end)
                  |> Enum.filter(fn 
                    %{ name: "" } -> false
                    _ -> true
                  end)
        {:ok, results}
        {:ok, %HTTPoison.Response{status_code: 404}} ->
        {:error, "Not found." }
        {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, reason}
    end
  end
end

A  => lib/shop_local/oregon_coffee_and_tea_search_provider.ex +34 -0
@@ 1,34 @@
defmodule ShopLocal.OregonCoffeeAndTeaSearchProvider do

  def search(s) do
    case HTTPoison.get("https://oregoncoffeeandtea.com/api/search/GeneralSearch?q=#{s}&p=0") do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        body = Poison.decode!(body)
        results = body["items"]
                  |> Enum.filter(fn
                    %{"recordTypeName" => "store-item"} -> true
                    _ -> false
                  end)
                  |> Enum.map(fn(k) -> 
                    link = HtmlSanitizeEx.strip_tags(k["itemUrl"])
                    link = case String.starts_with?(link, "http") do
                      false -> "https://oregoncoffeeandtea.com" <> link
                      true ->  link
                    end
                      %{
                        provider: "Oregon Coffee & Tea",
                        name: HtmlSanitizeEx.strip_tags(k["title"]),
                        desc: HtmlSanitizeEx.strip_tags(k["exerpt"]),
                        price: "$XXX",
                        link: link,
                        image_src: HtmlSanitizeEx.strip_tags(k["imageUrl"])
                      } 
                  end)
        {:ok, results}
      {:ok, %HTTPoison.Response{status_code: 404}} ->
        {:error, "Not found." }
      {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, reason}
    end
  end
end

A  => lib/shop_local/pegasus_search_provider.ex +28 -0
@@ 1,28 @@
defmodule ShopLocal.PegasusSearchProvider do

  def search(s) do
    case HTTPoison.post("https://pegasusgames.com/shop/search", "s=#{s}&resultsPerPage=10", [
      "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
      "Accept": "application/json, text/javascript, */*; q=0.01",
      "Accept-Encoding": "gzip, deflate, br",
      "Origin": "https://pegasusgames.com",
      "Referer": "https://pegasusgames.com/shop/search?controller=search&s=search",
    ]) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        body = Poison.decode!(body)
        results = Enum.map(body["products"], fn(k) -> %{
          provider: "Pegasus Games",
          name: HtmlSanitizeEx.strip_tags(k["name"]),
          desc: HtmlSanitizeEx.strip_tags(k["description_short"]),
          price: HtmlSanitizeEx.strip_tags(k["price"]),
          link: HtmlSanitizeEx.strip_tags(k["link"]),
          image_src: HtmlSanitizeEx.strip_tags(k["cover"]["small"]["url"])
        } end)
        {:ok, results}
      {:ok, %HTTPoison.Response{status_code: 404}} ->
        {:error, "Not found." }
      {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, reason}
    end
  end
end

A  => lib/shop_local/restyle_corvallis_search_provider.ex +34 -0
@@ 1,34 @@
defmodule ShopLocal.RestyleCorvallisSearchProvider do

  def search(s) do
    case HTTPoison.get("https://www.restylecorvallis.com/api/search/GeneralSearch?&q=#{s}&p=0") do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        body = Poison.decode!(body)
        results = body["items"]
                  |> Enum.filter(fn
                    %{"recordTypeName" => "store-item"} -> true
                    _ -> false
                  end)
                  |> Enum.map(fn(k) -> 
                    link = HtmlSanitizeEx.strip_tags(k["itemUrl"])
                    link = case String.starts_with?(link, "http") do
                      false -> "https://www.restylecorvallis.com" <> link
                      true ->  link
                    end
                      %{
                        provider: "Restyle (Corvallis)",
                        name: HtmlSanitizeEx.strip_tags(k["title"]),
                        desc: HtmlSanitizeEx.strip_tags(k["exerpt"]),
                        price: "$XXX",
                        link: link,
                        image_src: HtmlSanitizeEx.strip_tags(k["imageUrl"])
                      } 
                  end)
        {:ok, results}
      {:ok, %HTTPoison.Response{status_code: 404}} ->
        {:error, "Not found." }
      {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, reason}
    end
  end
end

A  => lib/shop_local/robnetts_search_provider.ex +59 -0
@@ 1,59 @@
defmodule ShopLocal.RobnettsSearchProvider do

  def search(s) do
    case HTTPoison.get("https://www.catalog-display.com/index.aspx?StId=1554&ShId=097fae5a200343bbb7228355c8b1597a&tab=21&Qtype=3&SearchBy=KeyWord&Val=#{s}") do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        {:ok, document} = Floki.parse_document(body)
        results = document
                  |> Floki.find("#result table .MinHeight350Product")
                  |> Enum.map(fn (k) -> 
                    name = k
                           |> Floki.find("div div:first-child strong span")
                           |> Floki.text
                    link = k
                           |> Floki.find("a.link") 
                           |> Floki.attribute("href")
                           |> List.last

                    image_src = k
                                |> Floki.find("a.link img")
                                |> Floki.attribute("src")

                    desc = k
                           |> Floki.find("*[id$=\"lblVendor\"],")
                           |> Floki.text

                    price = k
                      |> Floki.find(".cprice")
                      |> Floki.text
                      |> String.replace(["Price", "BAG"], "")
                      |> String.replace("USD ", "$")
                      |> String.split("/")
                      |> List.first

                    link = case String.starts_with?(link, "http") do
                      false -> "https://www.catalog-display.com" <> link
                      true ->  link
                    end

                      %{
                        provider: "Robnetts",
                        name: name,
                        desc: desc,
                        price: price,
                        link: link,
                        image_src: image_src
                      } 
                  end)
                  |> Enum.filter(fn 
                    %{ name: "" } -> false
                    _ -> true
                  end)
        {:ok, results}
        {:ok, %HTTPoison.Response{status_code: 404}} ->
        {:error, "Not found." }
        {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, reason}
    end
  end
end

A  => lib/shop_local/toy_factory_search_provider.ex +42 -0
@@ 1,42 @@
defmodule ShopLocal.ToyFactorySearchProvider do

  def search(s) do
    case HTTPoison.get("https://www.thetoyfactory.us/?s=#{s}") do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        {:ok, document} = Floki.parse_document(body)
        [{ _tag, _attributes, main_children }] = Floki.find(document, "#main")
        results = main_children
                  |> Enum.filter(fn
                    {"article", _, _ } -> true
                    _ -> false
                  end)
                  |> Enum.map(fn (k) -> 
                    name = k
                           |> Floki.find(".entry-title a") 
                           |> Floki.text
                    link = k
                           |> Floki.find(".entry-title a") 
                           |> Floki.attribute("href")
                    desc = k
                           |> Floki.find(".entry-summary") 
                           |> Floki.text
                    image_src = k
                                |> Floki.find(".post-image img")
                                |> Floki.attribute("src")
                    %{
                      provider: "Toy Factory",
                      name: name,
                      desc: desc,
                      price: "$XXX",
                      link: link,
                      image_src: image_src
                    } 
                  end)
      {:ok, results}
      {:ok, %HTTPoison.Response{status_code: 404}} ->
      {:error, "Not found." }
      {:error, %HTTPoison.Error{reason: reason}} ->
      {:error, reason}
    end
  end
end

A  => lib/shop_local_web.ex +81 -0
@@ 1,81 @@
defmodule ShopLocalWeb do
  @moduledoc """
  The entrypoint for defining your web interface, such
  as controllers, views, channels and so on.

  This can be used in your application as:

      use ShopLocalWeb, :controller
      use ShopLocalWeb, :view

  The definitions below will be executed for every view,
  controller, etc, so keep them short and clean, focused
  on imports, uses and aliases.

  Do NOT define functions inside the quoted expressions
  below. Instead, define any helper function in modules
  and import those modules here.
  """

  def controller do
    quote do
      use Phoenix.Controller, namespace: ShopLocalWeb

      import Plug.Conn
      import ShopLocalWeb.Gettext
      alias ShopLocalWeb.Router.Helpers, as: Routes
    end
  end

  def view do
    quote do
      use Phoenix.View,
        root: "lib/shop_local_web/templates",
        namespace: ShopLocalWeb

      # Import convenience functions from controllers
      import Phoenix.Controller,
        only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]

      # Include shared imports and aliases for views
      unquote(view_helpers())
    end
  end

  def router do
    quote do
      use Phoenix.Router

      import Plug.Conn
      import Phoenix.Controller
    end
  end

  def channel do
    quote do
      use Phoenix.Channel
      import ShopLocalWeb.Gettext
    end
  end

  defp view_helpers do
    quote do
      # Use all HTML functionality (forms, tags, etc)
      use Phoenix.HTML

      # Import basic rendering functionality (render, render_layout, etc)
      import Phoenix.View

      import ShopLocalWeb.ErrorHelpers
      import ShopLocalWeb.Gettext
      alias ShopLocalWeb.Router.Helpers, as: Routes
    end
  end

  @doc """
  When used, dispatch to the appropriate controller/view/etc.
  """
  defmacro __using__(which) when is_atom(which) do
    apply(__MODULE__, which, [])
  end
end

A  => lib/shop_local_web/channels/user_socket.ex +35 -0
@@ 1,35 @@
defmodule ShopLocalWeb.UserSocket do
  use Phoenix.Socket

  ## Channels
  # channel "room:*", ShopLocalWeb.RoomChannel

  # Socket params are passed from the client and can
  # be used to verify and authenticate a user. After
  # verification, you can put default assigns into
  # the socket that will be set for all channels, ie
  #
  #     {:ok, assign(socket, :user_id, verified_user_id)}
  #
  # To deny connection, return `:error`.
  #
  # See `Phoenix.Token` documentation for examples in
  # performing token verification on connect.
  @impl true
  def connect(_params, socket, _connect_info) do
    {:ok, socket}
  end

  # Socket id's are topics that allow you to identify all sockets for a given user:
  #
  #     def id(socket), do: "user_socket:#{socket.assigns.user_id}"
  #
  # Would allow you to broadcast a "disconnect" event and terminate
  # all active sockets and channels for a given user:
  #
  #     ShopLocalWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
  #
  # Returning `nil` makes this socket anonymous.
  @impl true
  def id(_socket), do: nil
end

A  => lib/shop_local_web/controllers/page_controller.ex +21 -0
@@ 1,21 @@
defmodule ShopLocalWeb.PageController do
  use ShopLocalWeb, :controller
  alias ShopLocalWeb.PageController
  alias ShopLocal.CombinedSearch

  require Logger

  def index(conn, _params) do
    render(conn, "index.html")
  end

  def search(conn, %{ "search" => s }) do
    case CombinedSearch.search(URI.encode_www_form(s)) do
      { :ok, results } ->
        render(conn, "index.html", %{results: results})
      { :error, message } ->
        Logger.warn(message);
        render(conn, "index.html", %{results: []})
    end
  end
end

A  => lib/shop_local_web/endpoint.ex +53 -0
@@ 1,53 @@
defmodule ShopLocalWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :shop_local

  # The session will be stored in the cookie and signed,
  # this means its contents can be read but not tampered with.
  # Set :encryption_salt if you would also like to encrypt it.
  @session_options [
    store: :cookie,
    key: "_shop_local_key",
    signing_salt: "2j+nMfHj"
  ]

  socket "/socket", ShopLocalWeb.UserSocket,
    websocket: true,
    longpoll: false

  socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]

  # Serve at "/" the static files from "priv/static" directory.
  #
  # You should set gzip to true if you are running phx.digest
  # when deploying your static files in production.
  plug Plug.Static,
    at: "/",
    from: :shop_local,
    gzip: false,
    only: ~w(css fonts images js favicon.ico robots.txt)

  # Code reloading can be explicitly enabled under the
  # :code_reloader configuration of your endpoint.
  if code_reloading? do
    socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
    plug Phoenix.LiveReloader
    plug Phoenix.CodeReloader
  end

  plug Phoenix.LiveDashboard.RequestLogger,
    param_key: "request_logger",
    cookie_key: "request_logger"

  plug Plug.RequestId
  plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

  plug Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Phoenix.json_library()

  plug Plug.MethodOverride
  plug Plug.Head
  plug Plug.Session, @session_options
  plug ShopLocalWeb.Router
end

A  => lib/shop_local_web/gettext.ex +24 -0
@@ 1,24 @@
defmodule ShopLocalWeb.Gettext do
  @moduledoc """
  A module providing Internationalization with a gettext-based API.

  By using [Gettext](https://hexdocs.pm/gettext),
  your module gains a set of macros for translations, for example:

      import ShopLocalWeb.Gettext

      # Simple translation
      gettext("Here is the string to translate")

      # Plural translation
      ngettext("Here is the string to translate",
               "Here are the strings to translate",
               3)

      # Domain-based translation
      dgettext("errors", "Here is the error message to translate")

  See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
  """
  use Gettext, otp_app: :shop_local
end

A  => lib/shop_local_web/router.ex +42 -0
@@ 1,42 @@
defmodule ShopLocalWeb.Router do
  use ShopLocalWeb, :router

  import Plug.BasicAuth

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  pipeline :auth do
    plug :basic_auth, Application.compile_env(:shop_local, :basic_auth)
  end

  scope "/", ShopLocalWeb do
    pipe_through :browser

    get "/", PageController, :index
    post "/", PageController, :search
  end

  # Other scopes may use custom stacks.
  # scope "/api", ShopLocalWeb do
  #   pipe_through :api
  # end


  import Phoenix.LiveDashboard.Router

  scope "/" do
    pipe_through :browser
    pipe_through :auth
    live_dashboard "/dashboard", metrics: ShopLocalWeb.Telemetry
  end
end

A  => lib/shop_local_web/telemetry.ex +48 -0
@@ 1,48 @@
defmodule ShopLocalWeb.Telemetry do
  use Supervisor
  import Telemetry.Metrics

  def start_link(arg) do
    Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
  end

  @impl true
  def init(_arg) do
    children = [
      # Telemetry poller will execute the given period measurements
      # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
      {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
      # Add reporters as children of your supervision tree.
      # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end

  def metrics do
    [
      # Phoenix Metrics
      summary("phoenix.endpoint.stop.duration",
        unit: {:native, :millisecond}
      ),
      summary("phoenix.router_dispatch.stop.duration",
        tags: [:route],
        unit: {:native, :millisecond}
      ),

      # VM Metrics
      summary("vm.memory.total", unit: {:byte, :kilobyte}),
      summary("vm.total_run_queue_lengths.total"),
      summary("vm.total_run_queue_lengths.cpu"),
      summary("vm.total_run_queue_lengths.io")
    ]
  end

  defp periodic_measurements do
    [
      # A module, function and arguments to be invoked periodically.
      # This function must call :telemetry.execute/3 and a metric must be added above.
      # {ShopLocalWeb, :count_users, []}
    ]
  end
end

A  => lib/shop_local_web/templates/layout/app.html.eex +23 -0
@@ 1,23 @@
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>ShopLocal</title>
    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
    <script defer type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </head>
  <body>
    <header>
      <section class="container">
        <h1>Shop Local</h2>
      </section>
    </header>
    <main role="main" class="container">
      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
      <%= @inner_content %>
    </main>
  </body>
</html>

A  => lib/shop_local_web/templates/page/index.html.eex +70 -0
@@ 1,70 @@
<section class="phx-hero">
  <h3></h3>
  <%= form_for @conn, Routes.page_path(@conn, :search), fn f -> %>
    <%= text_input f, :search, placeholder: "What are you looking for?" %>
    <%= submit "Search" %>
  <% end %>
</section>
<section class="row results">
  <ul>
  <%= if Map.has_key?(assigns, :results) do %>
    <%= for i <- @results do %>
      <li>
        <a href="<%= i.link %>">
          <b><%= i.name %></b>
          <i><%= i.price %></i>
          &mdash; <%= i.provider %>
          <div class="product-content">
            <div class="product-image-container">
              <%= if i.image_src != "" do %>
                <img 
                 class="product-image"
                 loading="lazy" 
                 alt="Product Image"
                 src="<%= i.image_src %>"
                >
               <% end %>
            </div>
            <p><%= i.desc %></p>
          </div>
        </a>
      </li>
    <% end %>
  <% end %>
  </ul>
</section>


<section class="row">
  <article class="column">
    <h2>Regions</h2>
    <ul>
      <li>
        <a href="https://hexdocs.pm/phoenix/overview.html">Guides &amp; Docs</a>
      </li>
      <li>
        <a href="https://github.com/phoenixframework/phoenix">Source</a>
      </li>
      <li>
        <a href="https://github.com/phoenixframework/phoenix/blob/v1.5/CHANGELOG.md">v1.5 Changelog</a>
      </li>
    </ul>
  </article>
  <article class="column">
    <h2>Stores</h2>
    <ul>
      <li>
        <a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
      </li>
      <li>
        <a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a>
      </li>
      <li>
        <a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
      </li>
      <li>
        <a href="https://elixir-slackin.herokuapp.com/">Elixir on Slack</a>
      </li>
    </ul>
  </article>
</section>

A  => lib/shop_local_web/views/error_helpers.ex +47 -0
@@ 1,47 @@
defmodule ShopLocalWeb.ErrorHelpers do
  @moduledoc """
  Conveniences for translating and building error messages.
  """

  use Phoenix.HTML

  @doc """
  Generates tag for inlined form input errors.
  """
  def error_tag(form, field) do
    Enum.map(Keyword.get_values(form.errors, field), fn error ->
      content_tag(:span, translate_error(error),
        class: "invalid-feedback",
        phx_feedback_for: input_id(form, field)
      )
    end)
  end

  @doc """
  Translates an error message using gettext.
  """
  def translate_error({msg, opts}) do
    # When using gettext, we typically pass the strings we want
    # to translate as a static argument:
    #
    #     # Translate "is invalid" in the "errors" domain
    #     dgettext("errors", "is invalid")
    #
    #     # Translate the number of files with plural rules
    #     dngettext("errors", "1 file", "%{count} files", count)
    #
    # Because the error messages we show in our forms and APIs
    # are defined inside Ecto, we need to translate them dynamically.
    # This requires us to call the Gettext module passing our gettext
    # backend as first argument.
    #
    # Note we use the "errors" domain, which means translations
    # should be written to the errors.po file. The :count option is
    # set by Ecto and indicates we should also apply plural rules.
    if count = opts[:count] do
      Gettext.dngettext(ShopLocalWeb.Gettext, "errors", msg, msg, count, opts)
    else
      Gettext.dgettext(ShopLocalWeb.Gettext, "errors", msg, opts)
    end
  end
end

A  => lib/shop_local_web/views/error_view.ex +16 -0
@@ 1,16 @@
defmodule ShopLocalWeb.ErrorView do
  use ShopLocalWeb, :view

  # If you want to customize a particular status code
  # for a certain format, you may uncomment below.
  # def render("500.html", _assigns) do
  #   "Internal Server Error"
  # end

  # By default, Phoenix returns the status message from
  # the template name. For example, "404.html" becomes
  # "Not Found".
  def template_not_found(template, _assigns) do
    Phoenix.Controller.status_message_from_template(template)
  end
end

A  => lib/shop_local_web/views/layout_view.ex +3 -0
@@ 1,3 @@
defmodule ShopLocalWeb.LayoutView do
  use ShopLocalWeb, :view
end

A  => lib/shop_local_web/views/page_view.ex +3 -0
@@ 1,3 @@
defmodule ShopLocalWeb.PageView do
  use ShopLocalWeb, :view
end

A  => mix.exs +63 -0
@@ 1,63 @@
defmodule ShopLocal.MixProject do
  use Mix.Project

  def project do
    [
      app: :shop_local,
      version: "0.1.0",
      elixir: "~> 1.7",
      elixirc_paths: elixirc_paths(Mix.env()),
      compilers: [:phoenix, :gettext] ++ Mix.compilers(),
      start_permanent: Mix.env() == :prod,
      aliases: aliases(),
      deps: deps()
    ]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [
      mod: {ShopLocal.Application, []},
      extra_applications: [:logger, :runtime_tools]
    ]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [
      {:phoenix, "~> 1.5.4"},
      {:phoenix_html, "~> 2.11"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:phoenix_live_dashboard, "~> 0.2"},
      {:telemetry_metrics, "~> 0.4"},
      {:telemetry_poller, "~> 0.4"},
      {:gettext, "~> 0.11"},
      {:jason, "~> 1.0"},
      {:plug_cowboy, "~> 2.0"},
      {:httpoison, "~> 1.6"},
      {:poison, "~> 3.1"},
      {:html_sanitize_ex, "~> 1.4"},
      {:floki, "~> 0.29"}
    ]
  end

  # Aliases are shortcuts or tasks specific to the current project.
  # For example, to install project dependencies and perform other setup tasks, run:
  #
  #     $ mix setup
  #
  # See the documentation for `Mix` for more info on aliases.
  defp aliases do
    [
      setup: ["deps.get"]
    ]
  end
end

A  => mix.lock +37 -0
@@ 1,37 @@
%{
  "basic_auth": {:hex, :basic_auth, "2.2.5", "ec2c934e4943b63cfc7d6b01c6f3fa51ade2a518ca36c9c0caee18a90bf98c4e", [:mix], [{:plug, "~> 0.14 or ~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8b5f067bcfe48d7dc02d43c18ad9e9b54e630c2da720667ac8ed46979b54b7cb"},
  "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
  "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
  "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"},
  "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
  "file_system": {:hex, :file_system, "0.2.9", "545b9c9d502e8bfa71a5315fac2a923bd060fd9acb797fe6595f54b0f975fd32", [:mix], [], "hexpm", "3cf87a377fe1d93043adeec4889feacf594957226b4f19d5897096d6f61345d8"},
  "floki": {:hex, :floki, "0.29.0", "b1710d8c93a2f860dc2d7adc390dd808dc2fb8f78ee562304457b75f4c640881", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "008585ce64b9f74c07d32958ec9866f4b8a124bf4da1e2941b28e41384edaaad"},
  "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
  "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
  "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
  "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.1", "e8a67da405fe9f0d1be121a40a60f70811192033a5b8d00a95dddd807f5e053e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "68d92656f47cd73598c45ad2394561f025c8c65d146001b955fd7b517858962a"},
  "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"},
  "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
  "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
  "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
  "mime": {:hex, :mime, "1.4.0", "5066f14944b470286146047d2f73518cf5cca82f8e4815cf35d196b58cf07c47", [:mix], [], "hexpm", "75fa42c4228ea9a23f70f123c74ba7cece6a03b1fd474fe13f6a7a85c6ea4ff6"},
  "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
  "mochiweb": {:hex, :mochiweb, "2.20.1", "e4dbd0ed716f076366ecf62ada5755a844e1d95c781e8c77df1d4114be868cdf", [:rebar3], [], "hexpm", "d1aeee7870470d2fa9eae0b3d5ab6c33801aa2d82b10e9dade885c5c921b36aa"},
  "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
  "phoenix": {:hex, :phoenix, "1.5.6", "8298cdb4e0f943242ba8410780a6a69cbbe972fef199b341a36898dd751bdd66", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0dc4d39af1306b6aa5122729b0a95ca779e42c708c6fe7abbb3d336d5379e956"},
  "phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
  "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.3.6", "6d031e9e5fa8c671e582539e8acd549c4d6e0e90aa704f6644a4a1f5fb334608", [:mix], [{:ecto_psql_extras, "~> 0.4.1 or ~> 0.5", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.14.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "55c891eb9cb344d6685c21f452806f54be6a660bbc090c94f65f287e8b4de002"},
  "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.0", "f35f61c3f959c9a01b36defaa1f0624edd55b87e236b606664a556d6f72fd2e7", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "02c1007ae393f2b76ec61c1a869b1e617179877984678babde131d716f95b582"},
  "phoenix_live_view": {:hex, :phoenix_live_view, "0.14.8", "1419f2612d5623207bfd9760c110f46cc5be05173dd6b5dc019500f93b692027", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.3", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "889660bdc113a6fe84fd1b840443e86d9c6bd0b851e8f3a8581c916bbc7a2099"},
  "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
  "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"},
  "plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"},
  "plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"},
  "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"},
  "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
  "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
  "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
  "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"},
  "telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
  "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
}

A  => priv/gettext/en/LC_MESSAGES/errors.po +11 -0
@@ 1,11 @@
## `msgid`s in this file come from POT (.pot) files.
##
## Do not add, change, or remove `msgid`s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use `mix gettext.extract --merge` or `mix gettext.merge`
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: en\n"

A  => priv/gettext/errors.pot +10 -0
@@ 1,10 @@
## This is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here has no
## effect: edit them in PO (`.po`) files instead.


A  => run_prod.sh +9 -0
@@ 1,9 @@
# Initial setup
mix deps.get --only prod
MIX_ENV=prod mix compile

# Compile assets
mix phx.digest

# Finally run the server
PORT=4001 MIX_ENV=prod mix phx.server

A  => test/shop_local_web/controllers/page_controller_test.exs +8 -0
@@ 1,8 @@
defmodule ShopLocalWeb.PageControllerTest do
  use ShopLocalWeb.ConnCase

  test "GET /", %{conn: conn} do
    conn = get(conn, "/")
    assert html_response(conn, 200) =~ "Welcome to Phoenix!"
  end
end

A  => test/shop_local_web/views/error_view_test.exs +14 -0
@@ 1,14 @@
defmodule ShopLocalWeb.ErrorViewTest do
  use ShopLocalWeb.ConnCase, async: true

  # Bring render/3 and render_to_string/3 for testing custom views
  import Phoenix.View

  test "renders 404.html" do
    assert render_to_string(ShopLocalWeb.ErrorView, "404.html", []) == "Not Found"
  end

  test "renders 500.html" do
    assert render_to_string(ShopLocalWeb.ErrorView, "500.html", []) == "Internal Server Error"
  end
end

A  => test/shop_local_web/views/layout_view_test.exs +8 -0
@@ 1,8 @@
defmodule ShopLocalWeb.LayoutViewTest do
  use ShopLocalWeb.ConnCase, async: true

  # When testing helpers, you may want to import Phoenix.HTML and
  # use functions such as safe_to_string() to convert the helper
  # result into an HTML string.
  # import Phoenix.HTML
end

A  => test/shop_local_web/views/page_view_test.exs +3 -0
@@ 1,3 @@
defmodule ShopLocalWeb.PageViewTest do
  use ShopLocalWeb.ConnCase, async: true
end

A  => test/support/channel_case.ex +34 -0
@@ 1,34 @@
defmodule ShopLocalWeb.ChannelCase do
  @moduledoc """
  This module defines the test case to be used by
  channel tests.

  Such tests rely on `Phoenix.ChannelTest` and also
  import other functionality to make it easier
  to build common data structures and query the data layer.

  Finally, if the test case interacts with the database,
  we enable the SQL sandbox, so changes done to the database
  are reverted at the end of every test. If you are using
  PostgreSQL, you can even run database tests asynchronously
  by setting `use ShopLocalWeb.ChannelCase, async: true`, although
  this option is not recommended for other databases.
  """

  use ExUnit.CaseTemplate

  using do
    quote do
      # Import conveniences for testing with channels
      import Phoenix.ChannelTest
      import ShopLocalWeb.ChannelCase

      # The default endpoint for testing
      @endpoint ShopLocalWeb.Endpoint
    end
  end

  setup _tags do
    :ok
  end
end

A  => test/support/conn_case.ex +37 -0
@@ 1,37 @@
defmodule ShopLocalWeb.ConnCase do
  @moduledoc """
  This module defines the test case to be used by
  tests that require setting up a connection.

  Such tests rely on `Phoenix.ConnTest` and also
  import other functionality to make it easier
  to build common data structures and query the data layer.

  Finally, if the test case interacts with the database,
  we enable the SQL sandbox, so changes done to the database
  are reverted at the end of every test. If you are using
  PostgreSQL, you can even run database tests asynchronously
  by setting `use ShopLocalWeb.ConnCase, async: true`, although
  this option is not recommended for other databases.
  """

  use ExUnit.CaseTemplate

  using do
    quote do
      # Import conveniences for testing with connections
      import Plug.Conn
      import Phoenix.ConnTest
      import ShopLocalWeb.ConnCase

      alias ShopLocalWeb.Router.Helpers, as: Routes

      # The default endpoint for testing
      @endpoint ShopLocalWeb.Endpoint
    end
  end

  setup _tags do
    {:ok, conn: Phoenix.ConnTest.build_conn()}
  end
end

A  => test/test_helper.exs +1 -0
@@ 1,1 @@
ExUnit.start()