diff --git a/assets/css/app.css b/assets/css/app.css index 1abadc7..0417463 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -5,7 +5,6 @@ @source "../css"; @source "../js"; @source "../../lib/mv_web"; -@source "../../deps/ash_authentication_phoenix"; /* A Tailwind plugin that makes "hero-#{ICON}" classes available. The heroicons installation itself is managed by your mix.exs */ diff --git a/config/config.exs b/config/config.exs index 43c8cf8..29a4211 100644 --- a/config/config.exs +++ b/config/config.exs @@ -76,25 +76,24 @@ config :esbuild, version: "0.17.11", mv: [ args: - ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), + ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/*), cd: Path.expand("../assets", __DIR__), env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} ] # Configure tailwind (the version is required) config :tailwind, - version: "3.4.3", + version: "4.0.9", mv: [ args: ~w( - --config=tailwind.config.js - --input=css/app.css - --output=../priv/static/assets/app.css + --input=assets/css/app.css + --output=priv/static/assets/css/app.css ), - cd: Path.expand("../assets", __DIR__) + cd: Path.expand("..", __DIR__) ] # Configures Elixir's Logger -config :logger, :console, +config :logger, :default_formatter, format: "$time $metadata[$level] $message\n", metadata: [:request_id] diff --git a/config/dev.exs b/config/dev.exs index 17b4ce1..51ed2f1 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -17,10 +17,10 @@ config :mv, Mv.Repo, # The watchers configuration can be used to run external # watchers to your application. For example, we can use it # to bundle .js and .css sources. -# Binding to loopback ipv4 address prevents access from other machines. config :mv, MvWeb.Endpoint, + # Binding to loopback ipv4 address prevents access from other machines. # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. - http: [ip: {127, 0, 0, 1}, port: 4000], + http: [ip: {127, 0, 0, 1}, port: String.to_integer(System.get_env("PORT") || "4000")], check_origin: false, code_reloader: true, debug_errors: true, @@ -56,10 +56,11 @@ config :mv, MvWeb.Endpoint, # Watch static and templates for browser reloading. config :mv, MvWeb.Endpoint, live_reload: [ + web_console_logger: true, patterns: [ ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", ~r"priv/gettext/.*(po)$", - ~r"lib/mv_web/(controllers|live|components)/.*(ex|heex)$" + ~r"lib/mv_web/(?:controllers|live|components|router)/?.*\.(ex|heex)$" ] ] @@ -67,7 +68,7 @@ config :mv, MvWeb.Endpoint, config :mv, dev_routes: true # Do not include metadata nor timestamps in development logs -config :logger, :console, format: "[$level] $message\n" +config :logger, :default_formatter, format: "[$level] $message\n" # Set a higher stacktrace during development. Avoid configuring such # in production as building large stacktraces may be expensive. @@ -77,7 +78,8 @@ config :phoenix, :stacktrace_depth, 20 config :phoenix, :plug_init_mode, :runtime config :phoenix_live_view, - # Include HEEx debug annotations as HTML comments in rendered markup + # Include HEEx debug annotations as HTML comments in rendered markup. + # Changing this configuration will require mix clean and a full recompile. debug_heex_annotations: true, # Enable helpful, but potentially expensive runtime checks enable_expensive_runtime_checks: true diff --git a/config/runtime.exs b/config/runtime.exs index e8ab249..464d8cd 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -116,7 +116,7 @@ if config_env() == :prod do # domain: System.get_env("MAILGUN_DOMAIN") # # For this example you need include a HTTP client required by Swoosh API client. - # Swoosh supports Hackney and Finch out of the box: + # Swoosh supports Hackney, Req and Finch out of the box: # # config :swoosh, :api_client, Swoosh.ApiClient.Hackney # diff --git a/lib/mv/application.ex b/lib/mv/application.ex index e0bf462..b77107e 100644 --- a/lib/mv/application.ex +++ b/lib/mv/application.ex @@ -12,8 +12,6 @@ defmodule Mv.Application do Mv.Repo, {DNSCluster, query: Application.get_env(:mv, :dns_cluster_query) || :ignore}, {Phoenix.PubSub, name: Mv.PubSub}, - # Start the Finch HTTP client for sending emails - {Finch, name: Mv.Finch}, {AshAuthentication.Supervisor, otp_app: :my}, # Start a worker by calling: Mv.Worker.start_link(arg) # {Mv.Worker, arg}, diff --git a/lib/mv_web.ex b/lib/mv_web.ex index 4254449..fff24b6 100644 --- a/lib/mv_web.ex +++ b/lib/mv_web.ex @@ -38,9 +38,7 @@ defmodule MvWeb do def controller do quote do - use Phoenix.Controller, - formats: [:html, :json], - layouts: [html: MvWeb.Layouts] + use Phoenix.Controller, formats: [:html, :json] use Gettext, backend: MvWeb.Gettext @@ -52,10 +50,8 @@ defmodule MvWeb do def live_view do quote do - use Phoenix.LiveView, - layout: {MvWeb.Layouts, :app} + use Phoenix.LiveView - on_mount MvWeb.LiveHelpers unquote(html_helpers()) end end @@ -91,8 +87,9 @@ defmodule MvWeb do # Core UI components import MvWeb.CoreComponents - # Shortcut for generating JS commands + # Common modules used in templates alias Phoenix.LiveView.JS + alias MvWeb.Layouts # Routes generation with the ~p sigil unquote(verified_routes()) diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index c35f1ce..656d3c0 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -3,92 +3,34 @@ defmodule MvWeb.CoreComponents do Provides core UI components. At first glance, this module may seem daunting, but its goal is to provide - core building blocks for your application, such as modals, tables, and - forms. The components consist mostly of markup and are well-documented + core building blocks for your application, such as tables, forms, and + inputs. The components consist mostly of markup and are well-documented with doc strings and declarative assigns. You may customize and style them in any way you want, based on your application growth and needs. - The default components use Tailwind CSS, a utility-first CSS framework. - See the [Tailwind CSS documentation](https://tailwindcss.com) to learn - how to customize them or feel free to swap in another framework altogether. + The foundation for styling is Tailwind CSS, a utility-first CSS framework, + augmented with daisyUI, a Tailwind CSS plugin that provides UI components + and themes. Here are useful references: + + * [daisyUI](https://daisyui.com/docs/intro/) - a good place to get + started and see the available components. + + * [Tailwind CSS](https://tailwindcss.com) - the foundational framework + we build on. You will use it for layout, sizing, flexbox, grid, and + spacing. + + * [Heroicons](https://heroicons.com) - see `icon/1` for usage. + + * [Phoenix.Component](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html) - + the component system used by Phoenix. Some components, such as `<.link>` + and `<.form>`, are defined there. - Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage. """ use Phoenix.Component use Gettext, backend: MvWeb.Gettext alias Phoenix.LiveView.JS - @doc """ - Renders a modal. - - ## Examples - - <.modal id="confirm-modal"> - This is a modal. - - - JS commands may be passed to the `:on_cancel` to configure - the closing/cancel event, for example: - - <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}> - This is another modal. - - - """ - attr :id, :string, required: true - attr :show, :boolean, default: false - attr :on_cancel, JS, default: %JS{} - slot :inner_block, required: true - - def modal(assigns) do - ~H""" - - """ - end - - @doc """ - Shows the flash group with standard titles and content. - - ## Examples - - <.flash_group flash={@flash} /> - """ - attr :flash, :map, required: true, doc: "the map of flash messages" - attr :id, :string, default: "flash-group", doc: "the optional id of flash container" - - def flash_group(assigns) do - ~H""" -
- <.flash kind={:info} title={gettext("Success!")} flash={@flash} /> - <.flash kind={:error} title={gettext("Error!")} flash={@flash} /> - <.flash - id="client-error" - kind={:error} - title={gettext("We can't find the internet")} - phx-disconnected={show(".phx-client-error #client-error")} - phx-connected={hide("#client-error")} - hidden - > - {gettext("Attempting to reconnect")} - <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" /> - - - <.flash - id="server-error" - kind={:error} - title={gettext("Something went wrong!")} - phx-disconnected={show(".phx-server-error #server-error")} - phx-connected={hide("#server-error")} - hidden - > - {gettext("Hang in there while we get back on track")} - <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" /> - -
- """ - end - - @doc """ - Renders a simple form. - - ## Examples - - <.simple_form for={@form} phx-change="validate" phx-submit="save"> - <.input field={@form[:email]} label="Email"/> - <.input field={@form[:username]} label="Username" /> - <:actions> - <.button>Save - - - """ - attr :for, :any, required: true, doc: "the data structure for the form" - attr :as, :any, default: nil, doc: "the server side parameter to collect all input under" - - attr :rest, :global, - include: ~w(autocomplete name rel action enctype method novalidate target multipart), - doc: "the arbitrary HTML attributes to apply to the form tag" - - slot :inner_block, required: true - slot :actions, doc: "the slot for form actions, such as a submit button" - - def simple_form(assigns) do - ~H""" - <.form :let={f} for={@for} as={@as} {@rest}> -
- {render_slot(@inner_block, f)} -
- {render_slot(action, f)} +
+ <.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" /> + <.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" /> +
+

{@title}

+

{msg}

+
+
- +
""" end @doc """ - Renders a button. + Renders a button with navigation support. ## Examples <.button>Send! - <.button phx-click="go" class="ml-2">Send! + <.button phx-click="go" variant="primary">Send! + <.button navigate={~p"/"}>Home """ - attr :type, :string, default: nil - attr :class, :string, default: nil - attr :rest, :global, include: ~w(disabled form name value) - + attr :rest, :global, include: ~w(href navigate patch method) + attr :variant, :string, values: ~w(primary) slot :inner_block, required: true - def button(assigns) do - ~H""" - - """ + def button(%{rest: rest} = assigns) do + variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"} + assigns = assign(assigns, :class, Map.fetch!(variants, assigns[:variant])) + + if rest[:href] || rest[:navigate] || rest[:patch] do + ~H""" + <.link class={["btn", @class]} {@rest}> + {render_slot(@inner_block)} + + """ + else + ~H""" + + """ + end end @doc """ @@ -276,7 +145,7 @@ defmodule MvWeb.CoreComponents do attr :type, :string, default: "text", values: ~w(checkbox color date datetime-local email file month number password - range search select tel text textarea time url week) + search select tel text textarea time url week) attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form, for example: @form[:email]" @@ -286,6 +155,8 @@ defmodule MvWeb.CoreComponents do attr :prompt, :string, default: nil, doc: "the prompt for select inputs" attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2" attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs" + attr :class, :string, default: nil, doc: "the input class to use over defaults" + attr :error_class, :string, default: nil, doc: "the input error class to use over defaults" attr :rest, :global, include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength @@ -309,108 +180,95 @@ defmodule MvWeb.CoreComponents do end) ~H""" -
-
+ """ end def input(%{type: "select"} = assigns) do ~H""" -
- <.label for={@id}>{@label} - +
+ <.error :for={msg <- @errors}>{msg} -
+ """ end def input(%{type: "textarea"} = assigns) do ~H""" -
- <.label for={@id}>{@label} - +
+ <.error :for={msg <- @errors}>{msg} -
+ """ end # All other inputs text, datetime-local, url, password, etc. are handled here... def input(assigns) do ~H""" -
- <.label for={@id}>{@label} - +
+ <.error :for={msg <- @errors}>{msg} -
+ """ end - @doc """ - Renders a label. - """ - attr :for, :string, default: nil - slot :inner_block, required: true - - def label(assigns) do + # Helper used by inputs to generate form errors + defp error(assigns) do ~H""" - - """ - end - - @doc """ - Generates a generic error message. - """ - slot :inner_block, required: true - - def error(assigns) do - ~H""" -

- <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" /> +

+ <.icon name="hero-exclamation-circle" class="size-5" /> {render_slot(@inner_block)}

""" @@ -427,12 +285,12 @@ defmodule MvWeb.CoreComponents do def header(assigns) do ~H""" -
+
-

+

{render_slot(@inner_block)}

-

+

{render_slot(@subtitle)}

@@ -473,49 +331,34 @@ defmodule MvWeb.CoreComponents do end ~H""" -
- - - - - - - - - - - - - -
{col[:label]} - {gettext("Actions")} -
-
- - - {render_slot(col, @row_item.(row))} - -
-
-
- - - {render_slot(action, @row_item.(row))} - -
-
-
+ + + + + + + + + + + + + +
{col[:label]} + {gettext("Actions")} +
+ {render_slot(col, @row_item.(row))} + +
+ <%= for action <- @action do %> + {render_slot(action, @row_item.(row))} + <% end %> +
+
""" end @@ -535,38 +378,14 @@ defmodule MvWeb.CoreComponents do def list(assigns) do ~H""" -
-
-
-
{item.title}
-
{render_slot(item)}
+
    +
  • +
    +
    {item.title}
    +
    {render_slot(item)}
    -
-
- """ - end - - @doc """ - Renders a back navigation link. - - ## Examples - - <.back navigate={~p"/posts"}>Back to posts - """ - attr :navigate, :any, required: true - slot :inner_block, required: true - - def back(assigns) do - ~H""" -
- <.link - navigate={@navigate} - class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700" - > - <.icon name="hero-arrow-left-solid" class="h-3 w-3" /> - {render_slot(@inner_block)} - -
+ + """ end @@ -581,15 +400,15 @@ defmodule MvWeb.CoreComponents do width, height, and background color classes. Icons are extracted from the `deps/heroicons` directory and bundled within - your compiled app.css by the plugin in your `assets/tailwind.config.js`. + your compiled app.css by the plugin in `assets/vendor/heroicons.js`. ## Examples - <.icon name="hero-x-mark-solid" /> - <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" /> + <.icon name="hero-x-mark" /> + <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" /> """ attr :name, :string, required: true - attr :class, :string, default: nil + attr :class, :string, default: "size-4" def icon(%{name: "hero-" <> _} = assigns) do ~H""" @@ -604,7 +423,7 @@ defmodule MvWeb.CoreComponents do to: selector, time: 300, transition: - {"transition-all transform ease-out duration-300", + {"transition-all ease-out duration-300", "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95", "opacity-100 translate-y-0 sm:scale-100"} ) @@ -615,37 +434,11 @@ defmodule MvWeb.CoreComponents do to: selector, time: 200, transition: - {"transition-all transform ease-in duration-200", - "opacity-100 translate-y-0 sm:scale-100", + {"transition-all ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100", "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"} ) end - def show_modal(js \\ %JS{}, id) when is_binary(id) do - js - |> JS.show(to: "##{id}") - |> JS.show( - to: "##{id}-bg", - time: 300, - transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"} - ) - |> show("##{id}-container") - |> JS.add_class("overflow-hidden", to: "body") - |> JS.focus_first(to: "##{id}-content") - end - - def hide_modal(js \\ %JS{}, id) do - js - |> JS.hide( - to: "##{id}-bg", - transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"} - ) - |> hide("##{id}-container") - |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"}) - |> JS.remove_class("overflow-hidden", to: "body") - |> JS.pop_focus() - end - @doc """ Translates an error message using gettext. """ diff --git a/lib/mv_web/components/layouts.ex b/lib/mv_web/components/layouts.ex index 1ced765..ba8ec67 100644 --- a/lib/mv_web/components/layouts.ex +++ b/lib/mv_web/components/layouts.ex @@ -40,6 +40,15 @@ defmodule MvWeb.Layouts do