Last active
October 3, 2024 13:14
-
-
Save Wigny/695b699e368a5b63c0be3b012db41ab9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"}, | |
{:phoenix, "~> 1.7.14"}, | |
{:phoenix_live_view, "~> 1.0.0-rc.6"}, | |
{:phoenix_html, "~> 4.1"}, | |
{:phoenix_ecto, "~> 4.6"}, | |
{:ecto, "~> 3.12"}, | |
{:ex_cldr, "~> 2.40"}, | |
{:ex_cldr_numbers, "~> 2.33"}, | |
{:ex_cldr_currencies, "~> 2.16"}, | |
{:ex_money, "~> 5.18", 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 ring-zinc-300 aria-[invalid=true]:ring-rose-400 focus:ring-zinc-400 aria-[invalid=true]:focus:ring-rose-400" | |
aria-invalid={to_string(Enum.any?(@errors))} | |
/> | |
<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), %{}) | |
not Map.has_key?(nested_params, "_unused_#{subkey}") | |
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