summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorhref <href@random.sh>2018-01-31 20:36:42 +0100
committerhref <href@random.sh>2018-01-31 20:36:42 +0100
commitfcb2a082346054108f3f16cffe968450cf961976 (patch)
tree8e7d2eb9eeebf854e62e08c29b147f34dfeb1881 /lib
import
Diffstat (limited to 'lib')
-rw-r--r--lib/lsg.ex9
-rw-r--r--lib/lsg/application.ex37
-rw-r--r--lib/lsg/icecast.ex117
-rw-r--r--lib/lsg/icecast_agent.ex17
-rw-r--r--lib/lsg_irc.ex2
-rw-r--r--lib/lsg_irc/broadcast_handler.ex43
-rw-r--r--lib/lsg_irc/connection_handler.ex36
-rw-r--r--lib/lsg_irc/login_handler.ex25
-rw-r--r--lib/lsg_irc/np_handler.ex32
-rw-r--r--lib/lsg_web.ex67
-rw-r--r--lib/lsg_web/channels/user_socket.ex37
-rw-r--r--lib/lsg_web/controllers/page_controller.ex20
-rw-r--r--lib/lsg_web/endpoint.ex57
-rw-r--r--lib/lsg_web/gettext.ex24
-rw-r--r--lib/lsg_web/router.ex26
-rw-r--r--lib/lsg_web/templates/layout/app.html.eex35
-rw-r--r--lib/lsg_web/templates/page/index.html.eex36
-rw-r--r--lib/lsg_web/templates/page/widget.html.eex11
-rw-r--r--lib/lsg_web/views/error_helpers.ex40
-rw-r--r--lib/lsg_web/views/error_view.ex17
-rw-r--r--lib/lsg_web/views/layout_view.ex3
-rw-r--r--lib/lsg_web/views/page_view.ex3
22 files changed, 694 insertions, 0 deletions
diff --git a/lib/lsg.ex b/lib/lsg.ex
new file mode 100644
index 0000000..d2076f8
--- /dev/null
+++ b/lib/lsg.ex
@@ -0,0 +1,9 @@
+defmodule LSG do
+ @moduledoc """
+ LSG 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
diff --git a/lib/lsg/application.ex b/lib/lsg/application.ex
new file mode 100644
index 0000000..73ec04d
--- /dev/null
+++ b/lib/lsg/application.ex
@@ -0,0 +1,37 @@
+defmodule LSG.Application do
+ use Application
+
+ # See https://hexdocs.pm/elixir/Application.html
+ # for more information on OTP Applications
+ def start(_type, _args) do
+ import Supervisor.Spec
+ {:ok, irc_client} = ExIRC.start_link!
+
+ # Define workers and child supervisors to be supervised
+ children = [
+ # Start the endpoint when the application starts
+ supervisor(LSGWeb.Endpoint, []),
+ # Start your own worker by calling: LSG.Worker.start_link(arg1, arg2, arg3)
+ # worker(LSG.Worker, [arg1, arg2, arg3]),
+ worker(Registry, [[keys: :duplicate, name: LSG.BroadcastRegistry]]),
+ worker(LSG.IcecastAgent, []),
+ worker(LSG.Icecast, []),
+ worker(LSG.IRC.ConnectionHandler, [irc_client]),
+ worker(LSG.IRC.LoginHandler, [irc_client]),
+ worker(LSG.IRC.BroadcastHandler, [irc_client]),
+ worker(LSG.IRC.NpHandler, [irc_client]),
+ ]
+
+ # See https://hexdocs.pm/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: LSG.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
+ LSGWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+end
diff --git a/lib/lsg/icecast.ex b/lib/lsg/icecast.ex
new file mode 100644
index 0000000..ec168c3
--- /dev/null
+++ b/lib/lsg/icecast.ex
@@ -0,0 +1,117 @@
+defmodule LSG.Icecast do
+ use GenServer
+ require Logger
+ @interval 10_000
+ @hackney_pool :default
+ @httpoison_opts [hackney: [pool: @hackney_pool]]
+ @fuse __MODULE__
+
+ def start_link, do: GenServer.start_link(__MODULE__, [], [])
+
+ def init(_) do
+ GenServer.cast(self(), :poll)
+ {:ok, nil}
+ end
+
+ def handle_cast(:poll, state) do
+ state = poll(state)
+ {:noreply, state}
+ end
+
+ def handle_info(:poll, state) do
+ state = poll(state)
+ {:noreply, state}
+ end
+
+ defp poll(state) do
+ state = case request(base_url(), :get) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ #update_json_stats(Jason.decode(body))
+ stats = update_stats(body)
+ if state != stats do
+ Logger.info "Icecast Update: " <> inspect(stats)
+ LSG.IcecastAgent.update(stats)
+ Registry.dispatch(LSG.BroadcastRegistry, "icecast", fn ws ->
+ for {pid, _} <- ws, do: send(pid, {:icecast, stats})
+ end)
+ stats
+ else
+ state
+ end
+ error ->
+ Logger.error "Icecast HTTP Error: #{inspect error}"
+ state
+ end
+ :timer.send_after(5_000, :poll)
+ state
+ end
+
+ defp update_stats(html) do
+ raw = Floki.find(html, "div.roundbox")
+ |> Enum.map(fn(html) ->
+ html = Floki.raw_html(html)
+ [{"h3", _, ["Mount Point /"<>mount]}] = Floki.find(html, "h3.mount")
+ stats = Floki.find(html, "tr")
+ |> Enum.map(fn({"tr", _, tds}) ->
+ [{"td", _, keys}, {"td", _, values}] = tds
+ key = List.first(keys)
+ value = List.first(values)
+ {key, value}
+ end)
+ |> Enum.into(Map.new)
+ {mount, stats}
+ end)
+ |> Enum.into(Map.new)
+
+ live? = if Map.get(raw["live"], "Content Type:", false), do: true, else: false
+ np = if live? do
+ raw["live"]["Currently playing:"]
+ else
+ raw["autodj"]["Currently playing:"]
+ end
+
+ genre = raw["live"]["Genre:"] || nil
+ %{np: np || "", live: live? || false, genre: genre}
+ end
+
+ defp update_json_stats({:ok, body}) do
+ Logger.debug "JSON STATS: #{inspect body}"
+ end
+
+ defp update_json_stats(error) do
+ Logger.error "Failed to decode JSON Stats: #{inspect error}"
+ end
+
+ defp request(uri, method, body \\ [], headers \\ []) do
+ headers = [{"user-agent", "LSG-API[lsg.goulag.org] href@random.sh"}] ++ headers
+ options = @httpoison_opts
+ case :ok do #:fuse.ask(@fuse, :sync) do
+ :ok -> run_request(method, uri, body, headers, options)
+ :blown -> :blown
+ end
+ end
+
+ # This is to work around hackney's behaviour of returning `{:error, :closed}` when a pool connection has been closed
+ # (keep-alive expired). We just retry the request immediatly up to five times.
+ defp run_request(method, uri, body, headers, options), do: run_request(method, uri, body, headers, options, 0)
+ defp run_request(method, uri, body, headers, options, retries) when retries < 4 do
+ case HTTPoison.request(method, uri, body, headers, options) do
+ {:error, :closed} -> run_request(method, uri, body, headers, options, retries + 1)
+ other -> other
+ end
+ end
+ defp run_request(method, uri, body, headers, options, _exceeded_retries), do: {:error, :unavailable}
+
+ #
+ # -- URIs
+ #
+
+ defp stats_json_url do
+ base_url() <> "/status-json.xsl"
+ end
+
+ defp base_url do
+ "http://91.121.59.45:8089"
+ end
+
+end
diff --git a/lib/lsg/icecast_agent.ex b/lib/lsg/icecast_agent.ex
new file mode 100644
index 0000000..8f8a86a
--- /dev/null
+++ b/lib/lsg/icecast_agent.ex
@@ -0,0 +1,17 @@
+defmodule LSG.IcecastAgent do
+ use Agent
+
+ def start_link() do
+ Agent.start_link(fn -> nil end, name: __MODULE__)
+ end
+
+ def update(stats) do
+ Agent.update(__MODULE__, fn(_old) -> stats end)
+ end
+
+ def get do
+ Agent.get(__MODULE__, fn(stats) -> stats end)
+ end
+
+end
+
diff --git a/lib/lsg_irc.ex b/lib/lsg_irc.ex
new file mode 100644
index 0000000..751c9c9
--- /dev/null
+++ b/lib/lsg_irc.ex
@@ -0,0 +1,2 @@
+defmodule LSG.IRC do
+end
diff --git a/lib/lsg_irc/broadcast_handler.ex b/lib/lsg_irc/broadcast_handler.ex
new file mode 100644
index 0000000..22ffbaf
--- /dev/null
+++ b/lib/lsg_irc/broadcast_handler.ex
@@ -0,0 +1,43 @@
+defmodule LSG.IRC.BroadcastHandler do
+ def start_link(client) do
+ GenServer.start_link(__MODULE__, [client])
+ end
+
+ def init([client]) do
+ ExIRC.Client.add_handler client, self
+ {:ok, _} = Registry.register(LSG.BroadcastRegistry, "icecast", [])
+ {:ok, {client, ["#lsg"], false}}
+ end
+
+ def handle_info({:icecast, stats = %{live: true}}, s = {client, channels, true}) do
+ for c <- channels do
+ ExIRC.Client.msg(client, :privmsg, c, "en direct: "<>format_genre(stats.genre) <> stats.np)
+ end
+ {:noreply, s}
+ end
+
+ def handle_info({:icecast, stats = %{live: true}}, s = {client, channels, false}) do
+ for c <- channels do
+ ExIRC.Client.msg(client, :privmsg, c, "115ANS EN DIREK! " <> format_genre(stats.genre) <> stats.np)
+ end
+ {:noreply, {client, channels, true}}
+ end
+
+ def handle_info({:icecast, stats = %{live: false}}, s = {client, channels, true}) do
+ for c <- channels do
+ ExIRC.Client.msg(client, :privmsg, c, "Le direct c'est fini, retour aux Smashing Pumpkins...")
+ ExIRC.Client.msg(client, :privmsg, c, "np: " <> stats.np)
+ end
+ {:noreply, {client, channels, false}}
+ end
+
+ # Catch-all for messages you don't care about
+ def handle_info(msg, state) do
+ {:noreply, state}
+ end
+
+ defp format_genre(nil), do: ""
+ defp format_genre(""), do: ""
+ defp format_genre(text), do: "[#{text}] "
+
+end
diff --git a/lib/lsg_irc/connection_handler.ex b/lib/lsg_irc/connection_handler.ex
new file mode 100644
index 0000000..f3bb1d4
--- /dev/null
+++ b/lib/lsg_irc/connection_handler.ex
@@ -0,0 +1,36 @@
+defmodule LSG.IRC.ConnectionHandler do
+ defmodule State do
+ defstruct host: "irc.quakenet.org",
+ port: 6667,
+ pass: "",
+ nick: "bot115ans",
+ user: "115ans",
+ name: "115ans.net",
+ client: nil
+ end
+
+ def start_link(client) do
+ GenServer.start_link(__MODULE__, [%State{client: client}])
+ end
+
+ def init([state]) do
+ ExIRC.Client.add_handler state.client, self
+ ExIRC.Client.connect! state.client, state.host, state.port
+ {:ok, state}
+ end
+
+ def handle_info({:connected, server, port}, state) do
+ debug "Connected to #{server}:#{port}"
+ ExIRC.Client.logon state.client, state.pass, state.nick, state.user, state.name
+ {:noreply, state}
+ end
+
+ # Catch-all for messages you don't care about
+ def handle_info(msg, state) do
+ {:noreply, state}
+ end
+
+ defp debug(msg) do
+ IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset()
+ end
+end
diff --git a/lib/lsg_irc/login_handler.ex b/lib/lsg_irc/login_handler.ex
new file mode 100644
index 0000000..b4757b1
--- /dev/null
+++ b/lib/lsg_irc/login_handler.ex
@@ -0,0 +1,25 @@
+defmodule LSG.IRC.LoginHandler do
+ def start_link(client) do
+ GenServer.start_link(__MODULE__, [client])
+ end
+
+ def init([client]) do
+ ExIRC.Client.add_handler client, self
+ {:ok, {client, ["#lsg"]}}
+ end
+
+ def handle_info(:logged_in, state = {client, channels}) do
+ debug "Logged in to server"
+ channels |> Enum.map(&ExIRC.Client.join client, &1)
+ {:noreply, state}
+ end
+
+ # Catch-all for messages you don't care about
+ def handle_info(_msg, state) do
+ {:noreply, state}
+ end
+
+ defp debug(msg) do
+ IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset()
+ end
+end
diff --git a/lib/lsg_irc/np_handler.ex b/lib/lsg_irc/np_handler.ex
new file mode 100644
index 0000000..8bde293
--- /dev/null
+++ b/lib/lsg_irc/np_handler.ex
@@ -0,0 +1,32 @@
+defmodule LSG.IRC.NpHandler do
+ def start_link(client) do
+ GenServer.start_link(__MODULE__, [client])
+ end
+
+ def init([client]) do
+ ExIRC.Client.add_handler client, self
+ {:ok, _} = Registry.register(LSG.BroadcastRegistry, "icecast", [])
+ {:ok, client}
+ end
+
+ def handle_info({:received, "!np", _, chan}, client) do
+ stats = LSG.IcecastAgent.get
+ np = if stats.live do
+ "en direct: #{format_genre(stats.genre)}#{stats.np}"
+ else
+ "np: #{stats.np}"
+ end
+ ExIRC.Client.msg(client, :privmsg, chan, np)
+ {:noreply, client}
+ end
+
+ # Catch-all for messages you don't care about
+ def handle_info(msg, client) do
+ {:noreply, client}
+ end
+
+ defp format_genre(nil), do: ""
+ defp format_genre(""), do: ""
+ defp format_genre(text), do: "[#{text}] "
+
+end
diff --git a/lib/lsg_web.ex b/lib/lsg_web.ex
new file mode 100644
index 0000000..e07d648
--- /dev/null
+++ b/lib/lsg_web.ex
@@ -0,0 +1,67 @@
+defmodule LSGWeb 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 LSGWeb, :controller
+ use LSGWeb, :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: LSGWeb
+ import Plug.Conn
+ import LSGWeb.Router.Helpers
+ import LSGWeb.Gettext
+ end
+ end
+
+ def view do
+ quote do
+ use Phoenix.View, root: "lib/lsg_web/templates",
+ namespace: LSGWeb
+
+ # Import convenience functions from controllers
+ import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
+
+ # Use all HTML functionality (forms, tags, etc)
+ use Phoenix.HTML
+
+ import LSGWeb.Router.Helpers
+ import LSGWeb.ErrorHelpers
+ import LSGWeb.Gettext
+ 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 LSGWeb.Gettext
+ 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
diff --git a/lib/lsg_web/channels/user_socket.ex b/lib/lsg_web/channels/user_socket.ex
new file mode 100644
index 0000000..6fedf18
--- /dev/null
+++ b/lib/lsg_web/channels/user_socket.ex
@@ -0,0 +1,37 @@
+defmodule LSGWeb.UserSocket do
+ use Phoenix.Socket
+
+ ## Channels
+ # channel "room:*", LSGWeb.RoomChannel
+
+ ## Transports
+ transport :websocket, Phoenix.Transports.WebSocket
+ # transport :longpoll, Phoenix.Transports.LongPoll
+
+ # 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.
+ def connect(_params, socket) 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:
+ #
+ # LSGWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
+ #
+ # Returning `nil` makes this socket anonymous.
+ def id(_socket), do: nil
+end
diff --git a/lib/lsg_web/controllers/page_controller.ex b/lib/lsg_web/controllers/page_controller.ex
new file mode 100644
index 0000000..3d4e444
--- /dev/null
+++ b/lib/lsg_web/controllers/page_controller.ex
@@ -0,0 +1,20 @@
+defmodule LSGWeb.PageController do
+ use LSGWeb, :controller
+
+ def index(conn, _params) do
+ render conn, "index.html"
+ end
+
+ def icecast(conn, _params) do
+ conn
+ |> json(LSG.IcecastAgent.get)
+ end
+
+ def widget(conn, options) do
+ icecast = LSG.IcecastAgent.get
+ conn
+ |> put_layout(false)
+ |> render("widget.html", icecast: icecast)
+ end
+
+end
diff --git a/lib/lsg_web/endpoint.ex b/lib/lsg_web/endpoint.ex
new file mode 100644
index 0000000..e05e3f5
--- /dev/null
+++ b/lib/lsg_web/endpoint.ex
@@ -0,0 +1,57 @@
+defmodule LSGWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :lsg
+
+ socket "/socket", LSGWeb.UserSocket
+
+ # Serve at "/" the static files from "priv/static" directory.
+ #
+ # You should set gzip to true if you are running phoenix.digest
+ # when deploying your static files in production.
+ plug Plug.Static,
+ at: "/assets", from: :lsg, 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 Plug.RequestId
+ plug Plug.Logger
+
+ plug Plug.Parsers,
+ parsers: [:urlencoded, :multipart, :json],
+ pass: ["*/*"],
+ json_decoder: Poison
+
+ plug Plug.MethodOverride
+ plug Plug.Head
+
+ # 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.
+ plug Plug.Session,
+ store: :cookie,
+ key: "_lsg_key",
+ signing_salt: "+p7K3wrj"
+
+ plug LSGWeb.Router
+
+ @doc """
+ Callback invoked for dynamically configuring the endpoint.
+
+ It receives the endpoint configuration and checks if
+ configuration should be loaded from the system environment.
+ """
+ def init(_key, config) do
+ if config[:load_from_system_env] do
+ port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
+ {:ok, Keyword.put(config, :http, [:inet6, port: port])}
+ else
+ {:ok, config}
+ end
+ end
+end
diff --git a/lib/lsg_web/gettext.ex b/lib/lsg_web/gettext.ex
new file mode 100644
index 0000000..f38a57d
--- /dev/null
+++ b/lib/lsg_web/gettext.ex
@@ -0,0 +1,24 @@
+defmodule LSGWeb.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 LSGWeb.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: :lsg
+end
diff --git a/lib/lsg_web/router.ex b/lib/lsg_web/router.ex
new file mode 100644
index 0000000..773a4c2
--- /dev/null
+++ b/lib/lsg_web/router.ex
@@ -0,0 +1,26 @@
+defmodule LSGWeb.Router do
+ use LSGWeb, :router
+
+ 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
+
+ scope "/", LSGWeb do
+ pipe_through :browser # Use the default browser stack
+
+ get "/embed/widget", PageController, :widget
+ end
+
+ scope "/api", LSGWeb do
+ pipe_through :api
+ get "/icecast.json", PageController, :icecast
+ end
+end
diff --git a/lib/lsg_web/templates/layout/app.html.eex b/lib/lsg_web/templates/layout/app.html.eex
new file mode 100644
index 0000000..0d91f12
--- /dev/null
+++ b/lib/lsg_web/templates/layout/app.html.eex
@@ -0,0 +1,35 @@
+<!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">
+ <meta name="description" content="">
+ <meta name="author" content="">
+
+ <title>Hello LSG!</title>
+ <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
+ </head>
+
+ <body>
+ <div class="container">
+ <header class="header">
+ <nav role="navigation">
+ <ul class="nav nav-pills pull-right">
+ <li><a href="http://www.phoenixframework.org/docs">Get Started</a></li>
+ </ul>
+ </nav>
+ <span class="logo"></span>
+ </header>
+
+ <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
+ <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
+
+ <main role="main">
+ <%= render @view_module, @view_template, assigns %>
+ </main>
+
+ </div> <!-- /container -->
+ <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
+ </body>
+</html>
diff --git a/lib/lsg_web/templates/page/index.html.eex b/lib/lsg_web/templates/page/index.html.eex
new file mode 100644
index 0000000..0988ea5
--- /dev/null
+++ b/lib/lsg_web/templates/page/index.html.eex
@@ -0,0 +1,36 @@
+<div class="jumbotron">
+ <h2><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h2>
+ <p class="lead">A productive web framework that<br />does not compromise speed and maintainability.</p>
+</div>
+
+<div class="row marketing">
+ <div class="col-lg-6">
+ <h4>Resources</h4>
+ <ul>
+ <li>
+ <a href="http://phoenixframework.org/docs/overview">Guides</a>
+ </li>
+ <li>
+ <a href="https://hexdocs.pm/phoenix">Docs</a>
+ </li>
+ <li>
+ <a href="https://github.com/phoenixframework/phoenix">Source</a>
+ </li>
+ </ul>
+ </div>
+
+ <div class="col-lg-6">
+ <h4>Help</h4>
+ <ul>
+ <li>
+ <a href="http://groups.google.com/group/phoenix-talk">Mailing list</a>
+ </li>
+ <li>
+ <a href="http://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on freenode IRC</a>
+ </li>
+ <li>
+ <a href="https://twitter.com/elixirphoenix">@elixirphoenix</a>
+ </li>
+ </ul>
+ </div>
+</div>
diff --git a/lib/lsg_web/templates/page/widget.html.eex b/lib/lsg_web/templates/page/widget.html.eex
new file mode 100644
index 0000000..efa382f
--- /dev/null
+++ b/lib/lsg_web/templates/page/widget.html.eex
@@ -0,0 +1,11 @@
+<!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">
+<link rel="stylesheet" href="<%= static_path(@conn, "/assets/css/widget.css") %>">
+<script src="<%= static_path(@conn, "/assets/js/widget.js") %>"></script>
+</head>
+<body></body>
+</html>
diff --git a/lib/lsg_web/views/error_helpers.ex b/lib/lsg_web/views/error_helpers.ex
new file mode 100644
index 0000000..47906f2
--- /dev/null
+++ b/lib/lsg_web/views/error_helpers.ex
@@ -0,0 +1,40 @@
+defmodule LSGWeb.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: "help-block"
+ end)
+ end
+
+ @doc """
+ Translates an error message using gettext.
+ """
+ def translate_error({msg, opts}) do
+ # Because error messages were defined within Ecto, we must
+ # call the Gettext module passing our Gettext backend. We
+ # also use the "errors" domain as translations are placed
+ # in the errors.po file.
+ # Ecto will pass the :count keyword if the error message is
+ # meant to be pluralized.
+ # On your own code and templates, depending on whether you
+ # need the message to be pluralized or not, this could be
+ # written simply as:
+ #
+ # dngettext "errors", "1 file", "%{count} files", count
+ # dgettext "errors", "is invalid"
+ #
+ if count = opts[:count] do
+ Gettext.dngettext(LSGWeb.Gettext, "errors", msg, msg, count, opts)
+ else
+ Gettext.dgettext(LSGWeb.Gettext, "errors", msg, opts)
+ end
+ end
+end
diff --git a/lib/lsg_web/views/error_view.ex b/lib/lsg_web/views/error_view.ex
new file mode 100644
index 0000000..1a7a92d
--- /dev/null
+++ b/lib/lsg_web/views/error_view.ex
@@ -0,0 +1,17 @@
+defmodule LSGWeb.ErrorView do
+ use LSGWeb, :view
+
+ def render("404.html", _assigns) do
+ "Page not found"
+ end
+
+ def render("500.html", _assigns) do
+ "Internal server error"
+ end
+
+ # In case no render clause matches or no
+ # template is found, let's render it as 500
+ def template_not_found(_template, assigns) do
+ render "500.html", assigns
+ end
+end
diff --git a/lib/lsg_web/views/layout_view.ex b/lib/lsg_web/views/layout_view.ex
new file mode 100644
index 0000000..39216ed
--- /dev/null
+++ b/lib/lsg_web/views/layout_view.ex
@@ -0,0 +1,3 @@
+defmodule LSGWeb.LayoutView do
+ use LSGWeb, :view
+end
diff --git a/lib/lsg_web/views/page_view.ex b/lib/lsg_web/views/page_view.ex
new file mode 100644
index 0000000..90c384c
--- /dev/null
+++ b/lib/lsg_web/views/page_view.ex
@@ -0,0 +1,3 @@
+defmodule LSGWeb.PageView do
+ use LSGWeb, :view
+end