Skip to content

Instantly share code, notes, and snippets.

@Wigny
Last active June 24, 2024 16:09
Show Gist options
  • Save Wigny/695b699e368a5b63c0be3b012db41ab9 to your computer and use it in GitHub Desktop.
Save Wigny/695b699e368a5b63c0be3b012db41ab9 to your computer and use it in GitHub Desktop.
Application.put_env(:input, Input.Endpoint,
server: true,
http: [ip: {127, 0, 0, 1}, port: 4001],
adapter: Bandit.PhoenixAdapter,
render_errors: [
formats: [html: Input.ErrorHTML],
layout: false
],
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
)
Application.put_env(:ex_money, :default_cldr_backend, Input.Cldr)
Mix.install([
{:bandit, "~> 1.5"},
{:jason, "~> 1.2"},
{:phoenix, "~> 1.7.12"},
{:phoenix_live_view, "~> 1.0.0-rc.5"},
{:phoenix_html, "~> 4.1"},
{:phoenix_ecto, "~> 4.6"},
{:ecto, "~> 3.11"},
{:ex_cldr, "~> 2.38"},
{:ex_cldr_numbers, "~> 2.33"},
{:ex_cldr_currencies, "~> 2.16"},
{:ex_money, "~> 5.16", runtime: false},
{:ex_money_sql, "~> 1.11"}
])
defmodule Input.ErrorHTML do
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end
defmodule Input.Layouts do
use Phoenix.Component
def render("root.html", assigns) do
~H"""
<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={Phoenix.Controller.get_csrf_token()} />
<.live_title>Input</.live_title>
<script
src={"https://unpkg.com/phoenix@#{Application.spec(:phoenix, :vsn)}/priv/static/phoenix.min.js"}
>
</script>
<script
src={"https://unpkg.com/phoenix_live_view@#{Application.spec(:phoenix_live_view, :vsn)}/priv/static/phoenix_live_view.min.js"}
>
</script>
<script src="https://cdn.tailwindcss.com?plugins=forms">
</script>
<script type="module">
const csrfToken = document
.querySelector("meta[name='csrf-token']")
.getAttribute("content");
const liveSocket = new LiveView.LiveSocket("/live", Phoenix.Socket, {
params: { _csrf_token: csrfToken }
});
liveSocket.connect();
</script>
</head>
<body class="bg-white antialiased">
<%= @inner_content %>
</body>
</html>
"""
end
def render("app.html", assigns) do
~H"""
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl">
<%= @inner_content %>
</div>
</main>
"""
end
end
defmodule Input.Cldr do
use Cldr,
locales: ["en", "pt"],
default_locale: "pt",
providers: [Cldr.Number, Money]
end
defmodule Input.CoreComponents do
use Phoenix.Component
def input(%{field: %Phoenix.HTML.FormField{} = field, type: "money"} = assigns) do
errors = if nested_used_input?(field, :amount), do: field.errors, else: []
assigns =
assigns
|> assign(:field, nil)
|> assign(:errors, Enum.map(errors, &translate_error/1))
|> assign_new(:id, fn -> field.id end)
|> assign_new(:name, fn -> field.name end)
|> assign_new(:value, fn -> field.value end)
|> update(:value, &money_value/1)
|> assign_new(:default_currency, fn ->
backend = Money.default_backend()
Cldr.Currency.current_currency_from_locale(backend.get_locale())
end)
~H"""
<div>
<.label for={@id}><%= @label %></.label>
<div class="relative mt-2 rounded-lg">
<input
type="text"
name={"#{@name}[amount]"}
id={"#{@id}_amount"}
value={@value[:amount]}
inputmode="numeric"
placeholder="0.00"
class={[
"block w-full rounded-lg border-0 pr-20 text-zinc-900 ring-1 ring-inset ring-zinc-300 placeholder:text-zinc-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6",
@errors == [] && "ring-zinc-300 focus:ring-zinc-400",
@errors != [] && "ring-rose-400 focus:ring-rose-400"
]}
/>
<div class="absolute inset-y-0 right-0 flex items-center">
<label for={"#{@id}_currency"} class="sr-only">Currency</label>
<select
id={"#{@id}_currency"}
name={"#{@name}[currency]"}
class="h-full rounded-lg border-0 bg-transparent py-0 pl-2 pr-7 text-zinc-500 focus:ring-1 focus:ring-inset focus:ring-zinc-600 sm:text-sm"
>
<%= Phoenix.HTML.Form.options_for_select(
Money.known_currencies(),
@value[:currency] || @default_currency
) %>
</select>
</div>
</div>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
defp money_value(nil) do
nil
end
defp money_value(money) when is_struct(money, Money) do
%{amount: money.amount, currency: money.currency}
end
defp money_value(%{"amount" => amount, "currency" => currency}) do
%{amount: amount, currency: currency}
end
def label(assigns) do
~H"""
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
<%= render_slot(@inner_block) %>
</label>
"""
end
def error(assigns) do
~H"""
<p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600">
<%= render_slot(@inner_block) %>
</p>
"""
end
def translate_error({msg, opts}) do
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
end)
end
def nested_used_input?(%Phoenix.HTML.FormField{field: field, form: form}, subkey) do
nested_params = Map.get(form.params, to_string(field), :unset)
cond do
nested_params == :unset -> false
not is_map_key(nested_params, "#{subkey}") -> false
is_map_key(nested_params, "_unused_#{subkey}") -> false
true -> true
end
end
end
defmodule Input.HomeLive do
use Phoenix.LiveView, layout: {Input.Layouts, :app}
import Input.CoreComponents
defmodule Data do
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field :price, Money.Ecto.Map.Type
end
def changeset(data, attrs) do
data
|> cast(attrs, [:price])
|> validate_required([:price])
end
end
def render(assigns) do
~H"""
<.form for={@form} phx-change="validate" phx-submit="save">
<div class="mt-10 space-y-8 bg-white">
<.input type="money" field={@form[:price]} label="Price" />
<div class="mt-2 flex items-center justify-between gap-6">
<button
type="submit"
class="phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3 text-sm font-semibold leading-6 text-white active:text-white/80"
>
Submit
</button>
</div>
</div>
</.form>
"""
end
def mount(_params, _session, socket) do
data = %Data{}
changeset = Data.changeset(data, %{})
{:ok,
socket
|> assign(:data, data)
|> assign(:form, to_form(changeset))}
end
def handle_event("validate", %{"data" => data_params}, socket) do
changeset = Data.changeset(socket.assigns.data, data_params)
{:noreply, assign(socket, :form, to_form(%{changeset | action: :validate}))}
end
def handle_event("save", %{"data" => data_params}, socket) do
changeset = Data.changeset(socket.assigns.data, data_params)
case Ecto.Changeset.apply_action(changeset, :insert) do
{:ok, data} -> {:noreply, assign(socket, :data, data)}
{:error, changeset} -> {:noreply, assign(socket, :form, to_form(changeset))}
end
end
end
defmodule Input.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {Input.Layouts, :root}
plug :protect_from_forgery
end
scope "/", Input do
pipe_through :browser
live "/", HomeLive, :index
end
end
defmodule Input.Endpoint do
use Phoenix.Endpoint, otp_app: :input
@session_options [
store: :cookie,
key: "_uploader_key",
signing_salt: "aaaaaaaa",
same_site: "Lax"
]
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
plug Plug.Session, @session_options
plug Input.Router
end
{:ok, _} = Supervisor.start_link([Input.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment