diff --git a/.gitignore b/.gitignore
index a58a312..baf5cbc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,13 +31,8 @@ birdy_chat-*.tar
# Ignore digested assets cache.
/priv/static/cache_manifest.json
-/priv/static/favicon-*.ico
-/priv/static/images/logo-*.svg
-/priv/static/robots-*.txt
-
# Ignore messages folder
/priv/messages/
-!priv/messages/README.md
# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
diff --git a/README.md b/README.md
index 8cfed24..5626ecc 100644
--- a/README.md
+++ b/README.md
@@ -1,60 +1,18 @@
-# BirdyChat Tech Challenge
+# BirdyChat
-This repository implements BirdyChat tech challenge.
+To start your Phoenix server:
-# Start here
+* Run `mix setup` to install and setup dependencies
+* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
-Firstly, check out this repository locally.
-Then install required versions of Elixir and Erlang from .tool-versions. asdf-vm will pick them up automatically.
-Then you run the test suite with `mix test`.
-Then, a scripted demo release is prepared in the prod environment:
+Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
-```
-MIX_ENV=prod mix build_release
-```
+Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
-Then you can run 3 servers connected to one another:
+## Learn more
-`_build/prod/rel/birdy_chat/bin/server_1` - runs at localhost:4001
-`_build/prod/rel/birdy_chat/bin/server_2` - runs at localhost:4002
-`_build/prod/rel/birdy_chat/bin/server_3` - runs at localhost:4003
-
-Out of the box they communicate with one another. Send a json request to one of them and observe the results. Feel free to modify the examples below.
-
-### A local request:
-
-```
-curl --request POST \
- --url http://localhost:4001/api/messages \
- --header 'content-type: application/json' \
- --data '{"message":"123","to":"1-user","from":"1-user"}'
-```
-
-### A remote request:
-
-```
-curl --request POST \
- --url http://localhost:4001/api/messages \
- --header 'content-type: application/json' \
- --data '{"message":"123","to":"2-user","from":"1-user"}'
-```
-
-### A request to unknown server:
-
-```
-curl --request POST \
- --url http://localhost:4001/api/messages \
- --header 'content-type: application/json' \
- --data '{"message":"123","to":"4-user","from":"1-user"}'
-```
-
-Files are saved to `priv/messages`.
-
-
-## Key rundown of technical details
-
-First and foremost, I tried to keep it as simple as possible - stick to know conventions, leverage existing libraries or frameworks, hence the usage of both Phoenix and Ecto - conventions they provide are understandable to pretty much any Elixir developer.
-
-All important modules are documented and there is a test suite that exacutes all known code paths.
-
-Servers authenticate with one another using Phoenix tokens. Servers communicate via HTTP using JSON. There were other options but I decided to use this one for reasons enumerated in documentation for BirdyChatWeb.Api.Server.Internal.Controller.
+* 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
diff --git a/config/config.exs b/config/config.exs
index 5dbfd9b..51096d6 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -21,6 +21,15 @@ config :birdy_chat, BirdyChatWeb.Endpoint,
pubsub_server: BirdyChat.PubSub,
live_view: [signing_salt: "6cr1V8uA"]
+# Configure the mailer
+#
+# By default it uses the "Local" adapter which stores the emails
+# locally. You can see the emails in your browser, at "/dev/mailbox".
+#
+# For production it's recommended to configure a different adapter
+# at the `config/runtime.exs`.
+config :birdy_chat, BirdyChat.Mailer, adapter: Swoosh.Adapters.Local
+
# Configure esbuild (the version is required)
config :esbuild,
version: "0.25.4",
diff --git a/config/dev.exs b/config/dev.exs
index dec572c..5138034 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -77,3 +77,6 @@ config :phoenix_live_view,
debug_attributes: true,
# Enable helpful, but potentially expensive runtime checks
enable_expensive_runtime_checks: true
+
+# Disable swoosh api client as it is only required for production adapters.
+config :swoosh, :api_client, false
diff --git a/config/prod.exs b/config/prod.exs
index ecdb679..4e08023 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -18,6 +18,12 @@ config :birdy_chat, BirdyChatWeb.Endpoint,
hosts: ["localhost", "127.0.0.1"]
]
+# Configure Swoosh API Client
+config :swoosh, api_client: Swoosh.ApiClient.Req
+
+# Disable Swoosh Local Memory Storage
+config :swoosh, local: false
+
# Do not print debug messages in production
config :logger, level: :info
diff --git a/config/runtime.exs b/config/runtime.exs
index 4c7f23e..26eec65 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -21,7 +21,7 @@ if System.get_env("PHX_SERVER") do
end
config :birdy_chat, BirdyChatWeb.Endpoint,
- http: [port: String.to_integer(System.get_env("BIRDY_CHAT_PORT", "4000"))]
+ http: [port: String.to_integer(System.get_env("PORT", "4000"))]
if config_env() == :prod do
# The secret key base is used to sign/encrypt cookies and other secrets.
@@ -82,4 +82,22 @@ if config_env() == :prod do
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.
+
+ # ## Configuring the mailer
+ #
+ # In production you need to configure the mailer to use a different adapter.
+ # Here is an example configuration for Mailgun:
+ #
+ # config :birdy_chat, BirdyChat.Mailer,
+ # adapter: Swoosh.Adapters.Mailgun,
+ # api_key: System.get_env("MAILGUN_API_KEY"),
+ # domain: System.get_env("MAILGUN_DOMAIN")
+ #
+ # Most non-SMTP adapters require an API client. Swoosh supports Req, Hackney,
+ # and Finch out-of-the-box. This configuration is typically done at
+ # compile-time in your config/prod.exs:
+ #
+ # config :swoosh, :api_client, Swoosh.ApiClient.Req
+ #
+ # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
end
diff --git a/config/test.exs b/config/test.exs
index 4a31ecc..3d83e54 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -7,6 +7,12 @@ config :birdy_chat, BirdyChatWeb.Endpoint,
secret_key_base: "DsOg8g/AU3wogZIm99JWnoDyijeinMJFFfkFdwkSkFcjvHywCXjCxl//NY1cvm7Y",
server: false
+# In test we don't send emails
+config :birdy_chat, BirdyChat.Mailer, adapter: Swoosh.Adapters.Test
+
+# Disable swoosh api client as it is only required for production adapters
+config :swoosh, :api_client, false
+
# Print only warnings and errors during test
config :logger, level: :warning
@@ -17,9 +23,6 @@ config :phoenix, :plug_init_mode, :runtime
config :phoenix_live_view,
enable_expensive_runtime_checks: true
-# Mock out HTTP requests in test
-config :birdy_chat, BirdyChat.Dispatcher, req_opts: [plug: {Req.Test, BirdyChat.Dispatcher}]
-
# Sort query params output of verified routes for robust url comparisons
config :phoenix,
sort_verified_routes_query_params: true
diff --git a/lib/birdy_chat/dispatcher.ex b/lib/birdy_chat/dispatcher.ex
deleted file mode 100644
index e0a3b7d..0000000
--- a/lib/birdy_chat/dispatcher.ex
+++ /dev/null
@@ -1,54 +0,0 @@
-defmodule BirdyChat.Dispatcher do
- @moduledoc """
- Main dispatcher of messages - decides to either write them to local file system or send them via
- HTTP to peers.
-
- It originally started as a websocket connection between servers, but then I decided to rip
- it out and replace with simple HTTP request-response for the following reasons:
-
- 1. HTTP guarantees immediate feedback (request succeeds or not), making the addition of caching
- or retries easy.
- 2. HTTP requests have well-know semantic so I can i.e use HTTP statuses for error signals
- instead of inventing my own error language.
- """
-
- @spec dispatch(Ecto.Changeset.t()) :: :ok | {:error, String.t()}
- def dispatch(%Ecto.Changeset{changes: changes} = changeset) do
- case changes do
- %{routing: :local} -> BirdyChat.MessageWriter.write(changeset.changes)
- %{routing: :remote} -> send_to_remote(changeset.changes)
- end
- end
-
- defp send_to_remote(%{server: server, from: from} = message) do
- {name, base_url} =
- BirdyChat.Identity.peers()
- |> Enum.find(fn {name, _url} -> name == server end)
-
- api_url = base_url <> "/api/internal"
- token = Phoenix.Token.sign(BirdyChatWeb.Endpoint, "serverAuth", from)
-
- {_request, result} =
- Req.new(url: api_url, retry: false, method: :post)
- |> Req.merge(req_opts())
- |> Req.Request.put_new_header("authorization", token)
- |> Req.merge(json: message)
- |> Req.Request.run_request()
-
- # Handle more when you encounter errors
- case result do
- %Req.Response{status: 201} -> :ok
- # This should never happen under normal circumstances so I am commenting this out but maybe
- # needs a second look.
- # %Req.Response{status: 403} -> {:error, "Unauthorised"}
-
- # Peer is down.
- %Req.TransportError{reason: :econnrefused} -> {:error, "peer #{name} is unreachable"}
- end
- end
-
- def req_opts do
- Application.get_env(:birdy_chat, __MODULE__)
- |> Access.get(:req_opts, [])
- end
-end
diff --git a/lib/birdy_chat/identity.ex b/lib/birdy_chat/identity.ex
index 3c62f1b..080386b 100644
--- a/lib/birdy_chat/identity.ex
+++ b/lib/birdy_chat/identity.ex
@@ -1,15 +1,4 @@
defmodule BirdyChat.Identity do
- @moduledoc """
- Server identity, populated on startup from the following environment variables:
-
- - BIRDY_CHAT_IDENTITY: Name of the server, a string that can be formatted into an integer.
- - BIRDY_CHAT_PEERS: List of peer servers in format of
- `1::http://localhost:4001;2::http://localhost:4002`
-
- When the environment variables are not present the server starts in test mode and can only
- communicate with itself.
- """
-
use Agent
defstruct [:identity, :peers, :mode]
@@ -60,18 +49,7 @@ defmodule BirdyChat.Identity do
peers = System.get_env("BIRDY_CHAT_PEERS")
case {identity, peers} do
- {nil, nil} ->
- %__MODULE__{
- identity: "test1",
- peers: %{"test2" => "http://localhost:4001"},
- mode: :test
- }
-
- {identity, peers} ->
- peers = parse_peers(peers)
- identity = parse_identity(identity)
-
- %__MODULE__{identity: identity, peers: peers, mode: :connected}
+ {nil, nil} -> %__MODULE__{identity: "test", peers: [], mode: :singleton}
end
end
end
diff --git a/lib/birdy_chat/mailer.ex b/lib/birdy_chat/mailer.ex
new file mode 100644
index 0000000..be833ef
--- /dev/null
+++ b/lib/birdy_chat/mailer.ex
@@ -0,0 +1,3 @@
+defmodule BirdyChat.Mailer do
+ use Swoosh.Mailer, otp_app: :birdy_chat
+end
diff --git a/lib/birdy_chat/message.ex b/lib/birdy_chat/message.ex
index a28e00c..3c86d43 100644
--- a/lib/birdy_chat/message.ex
+++ b/lib/birdy_chat/message.ex
@@ -1,39 +1,10 @@
defmodule BirdyChat.Message do
- @moduledoc """
- Main module for input validation. I decided to re-use Ecto because of existence of Phoenix.Ecto
- that clearly integrates the error messages produced from Ecto into HTTP plumbing of Phoenix.
- """
-
use Ecto.Schema
embedded_schema do
field :from, :string
field :to, :string
field :message, :string
- field :routing, Ecto.Enum, values: [:remote, :local]
- field :server, :string
- end
-
- @doc """
- Validation for inter-server communication. It is essentially the same as validate/1
- but without the requirement to communicate with home server only.
-
- This can be also designed as a function that accepts other function or some configuration option
- but two separately named functions are easier to understand and less prone to misuse.
- """
- def validate_for_inter_server_use(params) do
- changeset =
- %__MODULE__{}
- |> Ecto.Changeset.cast(params, [:from, :to, :message])
- |> Ecto.Changeset.validate_required([:from, :to, :message])
- |> put_routing()
- |> validate_is_local()
-
- if changeset.valid? do
- {:ok, changeset}
- else
- {:error, changeset}
- end
end
def validate(params) do
@@ -41,8 +12,6 @@ defmodule BirdyChat.Message do
%__MODULE__{}
|> Ecto.Changeset.cast(params, [:from, :to, :message])
|> Ecto.Changeset.validate_required([:from, :to, :message])
- |> put_routing()
- |> validate_home_server()
if changeset.valid? do
{:ok, changeset}
@@ -50,57 +19,4 @@ defmodule BirdyChat.Message do
{:error, changeset}
end
end
-
- defp validate_is_local(%Ecto.Changeset{changes: %{routing: :local}} = changeset) do
- changeset
- end
-
- defp validate_is_local(%Ecto.Changeset{} = changeset) do
- changeset
- |> Ecto.Changeset.add_error(:from, "you can only communicate with your home server")
- end
-
- defp validate_home_server(%Ecto.Changeset{changes: %{from: from}} = changeset) do
- identity = BirdyChat.Identity.identity()
-
- if String.starts_with?(from, identity) do
- changeset
- else
- changeset
- |> Ecto.Changeset.add_error(:from, "you can only communicate with your home server")
- end
- end
-
- defp validate_home_server(%Ecto.Changeset{} = changeset) do
- changeset
- end
-
- defp put_routing(%Ecto.Changeset{changes: %{to: to}} = changeset) do
- identity = BirdyChat.Identity.identity()
-
- if String.starts_with?(to, identity) do
- changeset
- |> Ecto.Changeset.put_change(:routing, :local)
- |> Ecto.Changeset.put_change(:server, identity)
- else
- server =
- BirdyChat.Identity.peers()
- |> Enum.find(fn {name, _url} -> String.starts_with?(to, name) end)
-
- case server do
- {name, _url} ->
- changeset
- |> Ecto.Changeset.put_change(:routing, :remote)
- |> Ecto.Changeset.put_change(:server, name)
-
- nil ->
- changeset
- |> Ecto.Changeset.add_error(:server, "unknown 'to' server")
- end
- end
- end
-
- defp put_routing(%Ecto.Changeset{} = changeset) do
- changeset
- end
end
diff --git a/lib/birdy_chat/message_writer.ex b/lib/birdy_chat/message_writer.ex
index c9d565a..b6f53aa 100644
--- a/lib/birdy_chat/message_writer.ex
+++ b/lib/birdy_chat/message_writer.ex
@@ -1,7 +1,5 @@
defmodule BirdyChat.MessageWriter do
- @moduledoc """
- Simple file writer that stores messages in priv folder of Elixir application/release.
- """
+ @moduledoc false
@spec write(%{to: String.t(), from: String.t(), message: String.t()}) :: :ok
def write(message) do
diff --git a/lib/birdy_chat_web.ex b/lib/birdy_chat_web.ex
index f38f95c..6d766a2 100644
--- a/lib/birdy_chat_web.ex
+++ b/lib/birdy_chat_web.ex
@@ -88,8 +88,8 @@ defmodule BirdyChatWeb do
import BirdyChatWeb.CoreComponents
# Common modules used in templates
- alias BirdyChatWeb.Layouts
alias Phoenix.LiveView.JS
+ alias BirdyChatWeb.Layouts
# Routes generation with the ~p sigil
unquote(verified_routes())
diff --git a/lib/birdy_chat_web/api/messages/controller.ex b/lib/birdy_chat_web/api/messages/controller.ex
index ee0693b..79afd64 100644
--- a/lib/birdy_chat_web/api/messages/controller.ex
+++ b/lib/birdy_chat_web/api/messages/controller.ex
@@ -1,23 +1,14 @@
defmodule BirdyChatWeb.Api.Messages.Controller do
- @moduledoc """
- The endpoint to be used by users from the "home server".
- """
-
use BirdyChatWeb, :controller
def create(conn, params) do
case BirdyChat.Message.validate(params) do
{:ok, changeset} ->
- case BirdyChat.Dispatcher.dispatch(changeset) do
+ case BirdyChat.MessageWriter.write(changeset.changes) do
:ok ->
conn
|> put_status(:created)
|> render(:create, message: changeset.changes)
-
- {:error, error} ->
- conn
- |> put_status(:unprocessable_entity)
- |> render(:error, message: error)
end
{:error, changeset} ->
diff --git a/lib/birdy_chat_web/api/messages/json.ex b/lib/birdy_chat_web/api/messages/json.ex
index 194bb7e..a238764 100644
--- a/lib/birdy_chat_web/api/messages/json.ex
+++ b/lib/birdy_chat_web/api/messages/json.ex
@@ -1,14 +1,8 @@
defmodule BirdyChatWeb.Api.Messages.JSON do
- @moduledoc false
-
def render("create.json", %{message: message}) do
message
end
- def render("error.json", %{message: message}) do
- %{errors: %{"general" => Gettext.dgettext(BirdyChatWeb.Gettext, "errors", message, [])}}
- end
-
def render("error.json", %{changeset: changeset}) do
errors = Ecto.Changeset.traverse_errors(changeset, &get_error/1)
%{errors: errors}
diff --git a/lib/birdy_chat_web/api/server/internal/controller.ex b/lib/birdy_chat_web/api/server/internal/controller.ex
deleted file mode 100644
index c368d5b..0000000
--- a/lib/birdy_chat_web/api/server/internal/controller.ex
+++ /dev/null
@@ -1,49 +0,0 @@
-defmodule BirdyChatWeb.Api.Server.Internal.Controller do
- @moduledoc """
- A controller for handling inter-server communication. It started off with using Erlang term
- format instead of JSON as communication language but then I removed it for the following
- reasons:
-
- 1. The messages are mostly binaries anyway, there is no big efficiency gain from skipping JSON.
- 2. Testing JSON is much easier than testing erlang term format.
- 3. Erlang term format can give an illusion of extra security but unless the transport is HTTPS
- then the communication is still inherently unsafe.
- 4. Erlang term format is difficult to handle for unfamiliar developers, you need to remember
- about safe conversion to avoid atom exhaustion attacks or sending an `rm -rf /` function over
- the wire.
-
- The endpoint is protected by simple authentication that requires the secret key of all servers
- being the same. It is good enough for a demo, but for any real application it would need to be
- reconsidered. HTTPS would be a non-negotiable requirement for any user-facing deployment.
- """
-
- use BirdyChatWeb, :controller
-
- def create(conn, params) do
- with true <- authorised?(conn.req_headers, params),
- {:ok, changeset} <- BirdyChat.Message.validate_for_inter_server_use(params),
- :ok <- BirdyChat.MessageWriter.write(changeset.changes) do
- conn
- |> put_status(:created)
- |> render(:create, message: changeset.changes)
- else
- _any ->
- conn
- |> put_status(:forbidden)
- |> render(:error, message: "Unauthorised")
- end
- end
-
- defp authorised?(headers, %{"from" => from}) do
- case Enum.find(headers, fn {key, _value} -> key == "authorization" end) do
- nil ->
- false
-
- {"authorization", token} ->
- case Phoenix.Token.verify(BirdyChatWeb.Endpoint, "serverAuth", token, max_age: 1200) do
- {:ok, id} -> id == from
- {:error, :invalid} -> false
- end
- end
- end
-end
diff --git a/lib/birdy_chat_web/api/server/internal/json.ex b/lib/birdy_chat_web/api/server/internal/json.ex
deleted file mode 100644
index 56352d7..0000000
--- a/lib/birdy_chat_web/api/server/internal/json.ex
+++ /dev/null
@@ -1,20 +0,0 @@
-defmodule BirdyChatWeb.Api.Server.Internal.JSON do
- @moduledoc false
-
- def render("create.json", %{message: message}) do
- message
- end
-
- def render("error.json", %{message: message}) do
- %{errors: %{"general" => Gettext.dgettext(BirdyChatWeb.Gettext, "errors", message, [])}}
- end
-
- def render("error.json", %{changeset: changeset}) do
- errors = Ecto.Changeset.traverse_errors(changeset, &get_error/1)
- %{errors: errors}
- end
-
- def get_error({msg, opts}) do
- Gettext.dgettext(BirdyChatWeb.Gettext, "errors", msg, opts)
- end
-end
diff --git a/lib/birdy_chat_web/channels/server_channel.ex b/lib/birdy_chat_web/channels/server_channel.ex
new file mode 100644
index 0000000..cb83c2c
--- /dev/null
+++ b/lib/birdy_chat_web/channels/server_channel.ex
@@ -0,0 +1,35 @@
+defmodule BirdyChatWeb.ServerChannel do
+ use BirdyChatWeb, :channel
+
+ @impl true
+ def join("server:" <> server_id, payload, socket) do
+ if authorised?(payload, server_id) do
+ {:ok, socket}
+ else
+ {:error, %{reason: "unauthorised"}}
+ end
+ end
+
+ @impl true
+ def handle_in("new_message", {:binary, message}, socket) do
+ result = :erlang.binary_to_term(message, [:safe])
+
+ # TODO: Add validation that this is the right server.
+ case BirdyChat.Message.validate(result) do
+ {:ok, changeset} ->
+ case BirdyChat.MessageWriter.write(changeset.changes) do
+ :ok ->
+ {:reply, {:ok, {:binary, message}}, socket}
+ end
+ end
+ end
+
+ # Simple token-based authentication. Servers should use the same Phoenix secret key so they will
+ # have the basis for generating tokens.
+ defp authorised?(%{"token" => token}, server_id) do
+ case Phoenix.Token.verify(BirdyChatWeb.Endpoint, "serverAuth", token, max_age: 86400) do
+ {:ok, id} -> id == server_id
+ {:error, :invalid} -> false
+ end
+ end
+end
diff --git a/lib/birdy_chat_web/channels/server_socket.ex b/lib/birdy_chat_web/channels/server_socket.ex
new file mode 100644
index 0000000..8843d1b
--- /dev/null
+++ b/lib/birdy_chat_web/channels/server_socket.ex
@@ -0,0 +1,44 @@
+defmodule BirdyChatWeb.ServerSocket do
+ use Phoenix.Socket
+
+ # A Socket handler
+ #
+ # It's possible to control the websocket connection and
+ # assign values that can be accessed by your channel topics.
+
+ ## Channels
+
+ channel "server:*", BirdyChatWeb.ServerChannel
+
+ # 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` or `{:error, term}`. To control the
+ # response the client receives in that case, [define an error handler in the
+ # websocket
+ # configuration](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration).
+ #
+ # 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 IDs 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:
+ #
+ # Elixir.BirdyChatWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
+ #
+ # Returning `nil` makes this socket anonymous.
+ @impl true
+ def id(socket), do: "user_socket:#{socket.assigns.server_id}"
+end
diff --git a/lib/birdy_chat_web/components/core_components.ex b/lib/birdy_chat_web/components/core_components.ex
index 59a56ef..b50a31b 100644
--- a/lib/birdy_chat_web/components/core_components.ex
+++ b/lib/birdy_chat_web/components/core_components.ex
@@ -29,7 +29,6 @@ defmodule BirdyChatWeb.CoreComponents do
use Phoenix.Component
use Gettext, backend: BirdyChatWeb.Gettext
- alias Phoenix.HTML.Form
alias Phoenix.LiveView.JS
@doc """
@@ -201,7 +200,9 @@ defmodule BirdyChatWeb.CoreComponents do
def input(%{type: "checkbox"} = assigns) do
assigns =
- assign_new(assigns, :checked, fn -> Form.normalize_value("checkbox", assigns[:value]) end)
+ assign_new(assigns, :checked, fn ->
+ Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
+ end)
~H"""
diff --git a/lib/birdy_chat_web/controllers/page_controller.ex b/lib/birdy_chat_web/controllers/page_controller.ex
new file mode 100644
index 0000000..6350031
--- /dev/null
+++ b/lib/birdy_chat_web/controllers/page_controller.ex
@@ -0,0 +1,7 @@
+defmodule BirdyChatWeb.PageController do
+ use BirdyChatWeb, :controller
+
+ def home(conn, _params) do
+ render(conn, :home)
+ end
+end
diff --git a/lib/birdy_chat_web/controllers/page_html.ex b/lib/birdy_chat_web/controllers/page_html.ex
new file mode 100644
index 0000000..fe2290c
--- /dev/null
+++ b/lib/birdy_chat_web/controllers/page_html.ex
@@ -0,0 +1,10 @@
+defmodule BirdyChatWeb.PageHTML do
+ @moduledoc """
+ This module contains pages rendered by PageController.
+
+ See the `page_html` directory for all templates available.
+ """
+ use BirdyChatWeb, :html
+
+ embed_templates "page_html/*"
+end
diff --git a/lib/birdy_chat_web/controllers/page_html/home.html.heex b/lib/birdy_chat_web/controllers/page_html/home.html.heex
new file mode 100644
index 0000000..b107fd0
--- /dev/null
+++ b/lib/birdy_chat_web/controllers/page_html/home.html.heex
@@ -0,0 +1,202 @@
+
+
+ Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
+