From f4aaf98e43fac55411ee3f8336593bc0dba98e2d Mon Sep 17 00:00:00 2001 From: Axel Clark Date: Mon, 19 Feb 2024 13:55:32 -0800 Subject: [PATCH 1/2] Add html and core_components --- lib/ex338_web.ex | 32 + lib/ex338_web/components/core_components.ex | 676 ++++++++++++++++++++ 2 files changed, 708 insertions(+) create mode 100644 lib/ex338_web/components/core_components.ex diff --git a/lib/ex338_web.ex b/lib/ex338_web.ex index f2df893d..ad53ac68 100644 --- a/lib/ex338_web.ex +++ b/lib/ex338_web.ex @@ -111,6 +111,38 @@ defmodule Ex338Web do end end + def html do + quote do + use Phoenix.Component + + # Import convenience functions from controllers + import Phoenix.Controller, + only: [get_csrf_token: 0, view_module: 1, view_template: 1] + + # include while migrating to Phoenix.Component + import Phoenix.View + + # Include general helpers for rendering HTML + unquote(html_helpers()) + end + end + + defp html_helpers do + quote do + # HTML escaping functionality + # Core UI components and translation + import Ex338Web.CoreComponents + import Ex338Web.Gettext + import Phoenix.HTML + + # Shortcut for generating JS commands + alias Phoenix.LiveView.JS + + # Routes generation with the ~p sigil + unquote(verified_routes()) + end + end + def verified_routes do quote do use Phoenix.VerifiedRoutes, diff --git a/lib/ex338_web/components/core_components.ex b/lib/ex338_web/components/core_components.ex new file mode 100644 index 00000000..027334af --- /dev/null +++ b/lib/ex338_web/components/core_components.ex @@ -0,0 +1,676 @@ +defmodule Ex338Web.CoreComponents do + @moduledoc """ + 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 + 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. + + Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage. + """ + use Phoenix.Component + + import Ex338Web.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 + + 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 + ~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" /> + <%= render_slot(@inner_block) %> +

+ """ + end + + @doc """ + Renders a header with title. + """ + attr :class, :string, default: nil + + slot :inner_block, required: true + slot :subtitle + slot :actions + + def header(assigns) do + ~H""" +
+
+

+ <%= render_slot(@inner_block) %> +

+

+ <%= render_slot(@subtitle) %> +

+
+
<%= render_slot(@actions) %>
+
+ """ + end + + @doc ~S""" + Renders a table with generic styling. + + ## Examples + + <.table id="users" rows={@users}> + <:col :let={user} label="id"><%= user.id %> + <:col :let={user} label="username"><%= user.username %> + + """ + attr :id, :string, required: true + attr :rows, :list, required: true + attr :row_id, :any, default: nil, doc: "the function for generating the row id" + attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row" + + attr :row_item, :any, + default: &Function.identity/1, + doc: "the function for mapping each row before calling the :col and :action slots" + + slot :col, required: true do + attr :label, :string + end + + slot :action, doc: "the slot for showing user actions in the last table column" + + def table(assigns) do + assigns = + with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do + assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) + end + + ~H""" +
+ + + + + + + + + + + + + +
<%= col[:label] %> + <%= gettext("Actions") %> +
+
+ + + <%= render_slot(col, @row_item.(row)) %> + +
+
+
+ + + <%= render_slot(action, @row_item.(row)) %> + +
+
+
+ """ + end + + @doc """ + Renders a data list. + + ## Examples + + <.list> + <:item title="Title"><%= @post.title %> + <:item title="Views"><%= @post.views %> + + """ + slot :item, required: true do + attr :title, :string, required: true + end + + def list(assigns) do + ~H""" +
+
+
+
<%= 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-gray-900 hover:text-gray-700" + > + <.icon name="hero-arrow-left-solid" class="h-3 w-3" /> + <%= render_slot(@inner_block) %> + +
+ """ + end + + @doc """ + Renders a [Heroicon](https://heroicons.com). + + Heroicons come in three styles – outline, solid, and mini. + By default, the outline style is used, but solid and mini may + be applied by using the `-solid` and `-mini` suffix. + + You can customize the size and colors of the icons by setting + 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`. + + ## Examples + + <.icon name="hero-x-mark-solid" /> + <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" /> + """ + attr :name, :string, required: true + attr :class, :string, default: nil + + def icon(%{name: "hero-" <> _} = assigns) do + ~H""" + + """ + end + + ## JS Commands + + def show(js \\ %JS{}, selector) do + JS.show(js, + to: selector, + transition: + {"transition-all transform ease-out duration-300", + "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95", + "opacity-100 translate-y-0 sm:scale-100"} + ) + end + + def hide(js \\ %JS{}, selector) do + JS.hide(js, + to: selector, + time: 200, + transition: + {"transition-all transform 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", + 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. + """ + def translate_error({msg, opts}) do + # When using gettext, we typically pass the strings we want + # to translate as a static argument: + # + # # Translate the number of files with plural rules + # dngettext("errors", "1 file", "%{count} files", count) + # + # However the error messages in our forms and APIs are generated + # dynamically, so we need to translate them by calling Gettext + # with our gettext backend as first argument. Translations are + # available in the errors.po file (as we use the "errors" domain). + if count = opts[:count] do + Gettext.dngettext(Ex338Web.Gettext, "errors", msg, msg, count, opts) + else + Gettext.dgettext(Ex338Web.Gettext, "errors", msg, opts) + end + end + + @doc """ + Translates the errors for a field from a keyword list of errors. + """ + def translate_errors(errors, field) when is_list(errors) do + for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) + end +end From 2200fe283112c4f64d35911655e57a10cf6d5908 Mon Sep 17 00:00:00 2001 From: Axel Clark Date: Mon, 19 Feb 2024 15:03:12 -0800 Subject: [PATCH 2/2] Convert owner controller to html and update core components --- lib/ex338_web.ex | 19 ++++++ lib/ex338_web/components/core_components.ex | 62 ++++++++++++++++++- lib/ex338_web/controllers/owner_controller.ex | 4 +- lib/ex338_web/controllers/owner_html.ex | 48 ++++++++++++++ lib/ex338_web/templates/owner/index.html.eex | 5 -- lib/ex338_web/templates/owner/table.html.eex | 34 ---------- lib/ex338_web/views/owner_view.ex | 3 - 7 files changed, 130 insertions(+), 45 deletions(-) create mode 100644 lib/ex338_web/controllers/owner_html.ex delete mode 100644 lib/ex338_web/templates/owner/index.html.eex delete mode 100644 lib/ex338_web/templates/owner/table.html.eex delete mode 100644 lib/ex338_web/views/owner_view.ex diff --git a/lib/ex338_web.ex b/lib/ex338_web.ex index ad53ac68..ac8d2495 100644 --- a/lib/ex338_web.ex +++ b/lib/ex338_web.ex @@ -35,6 +35,24 @@ defmodule Ex338Web do end end + def controller_html do + quote do + use Phoenix.Controller, + formats: [:html, :json], + layouts: [html: {Ex338Web.LayoutView, :app}] + + import Ecto + import Ecto.Query + import Ex338Web.Gettext + import Phoenix.LiveView.Controller + import Plug.Conn + + alias Ex338.Repo + + unquote(verified_routes()) + end + end + def view do quote do use Phoenix.View, @@ -133,6 +151,7 @@ defmodule Ex338Web do # Core UI components and translation import Ex338Web.CoreComponents import Ex338Web.Gettext + import Ex338Web.ViewHelpers import Phoenix.HTML # Shortcut for generating JS commands diff --git a/lib/ex338_web/components/core_components.ex b/lib/ex338_web/components/core_components.ex index 027334af..12e32408 100644 --- a/lib/ex338_web/components/core_components.ex +++ b/lib/ex338_web/components/core_components.ex @@ -468,7 +468,7 @@ defmodule Ex338Web.CoreComponents do slot :action, doc: "the slot for showing user actions in the last table column" - def table(assigns) do + def base_table(assigns) do assigns = with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) @@ -673,4 +673,64 @@ defmodule Ex338Web.CoreComponents do def translate_errors(errors, field) when is_list(errors) do for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) end + + attr :class, :string, default: nil + slot :inner_block, required: true + + def page_header(assigns) do + ~H""" +

+ <%= render_slot(@inner_block) %> +

+ """ + end + + attr :class, :string, default: nil + slot :inner_block, required: true + + def legacy_table(assigns) do + ~H""" +
+
+
+ + <%= render_slot(@inner_block) %> +
+
+
+
+ """ + end + + attr :class, :string, default: nil + slot :inner_block, required: true + + def legacy_th(assigns) do + ~H""" + + <%= render_slot(@inner_block) %> + + """ + end + + attr :class, :string, default: nil + attr :style, :string, default: nil + slot :inner_block, required: true + + def legacy_td(assigns) do + ~H""" + + <%= render_slot(@inner_block) %> + + """ + end end diff --git a/lib/ex338_web/controllers/owner_controller.ex b/lib/ex338_web/controllers/owner_controller.ex index 1d775126..ce1be7d9 100644 --- a/lib/ex338_web/controllers/owner_controller.ex +++ b/lib/ex338_web/controllers/owner_controller.ex @@ -1,5 +1,5 @@ defmodule Ex338Web.OwnerController do - use Ex338Web, :controller + use Ex338Web, :controller_html alias Ex338.FantasyLeagues.FantasyLeague alias Ex338.FantasyTeams.Owner @@ -15,7 +15,7 @@ defmodule Ex338Web.OwnerController do render( conn, - "index.html", + :index, fantasy_league: fantasy_league, owners: owners ) diff --git a/lib/ex338_web/controllers/owner_html.ex b/lib/ex338_web/controllers/owner_html.ex new file mode 100644 index 00000000..377378aa --- /dev/null +++ b/lib/ex338_web/controllers/owner_html.ex @@ -0,0 +1,48 @@ +defmodule Ex338Web.OwnerHTML do + use Ex338Web, :html + + def index(assigns) do + ~H""" + <.page_header class="sm:mb-6"> + League Owners + + + <.legacy_table> + + + <.legacy_th> + Fantasy Team + + <.legacy_th> + Owner + + <.legacy_th> + Slack Name + + + + + <%= for owner <- @owners do %> + + <.legacy_td class="text-indigo-700"> + <%= fantasy_team_link(@conn, owner.fantasy_team) %> + + <.legacy_td class="text-indigo-700"> + <.link href={~p"/users/#{owner.user.id}"}> + <%= owner.user.name %> + + + <.legacy_td> + <%= if owner.user.slack_name == "" do %> + -- + <% else %> + <%= owner.user.slack_name %> + <% end %> + + + <% end %> + + + """ + end +end diff --git a/lib/ex338_web/templates/owner/index.html.eex b/lib/ex338_web/templates/owner/index.html.eex deleted file mode 100644 index 48b9b4a3..00000000 --- a/lib/ex338_web/templates/owner/index.html.eex +++ /dev/null @@ -1,5 +0,0 @@ -<%= page_header class: "sm:mb-6" do %> - League Owners -<% end %> - -<%= render "table.html", assigns %> diff --git a/lib/ex338_web/templates/owner/table.html.eex b/lib/ex338_web/templates/owner/table.html.eex deleted file mode 100644 index 07be744a..00000000 --- a/lib/ex338_web/templates/owner/table.html.eex +++ /dev/null @@ -1,34 +0,0 @@ -<%= table do %> - - - <%= table_th do %> - Fantasy Team - <% end %> - <%= table_th do %> - Owner - <% end %> - <%= table_th do %> - Slack Name - <% end %> - - - - <%= for owner <- @owners do %> - - <%= table_td class: "text-indigo-700" do %> - <%= fantasy_team_link(@conn, owner.fantasy_team)%> - <% end %> - <%= table_td class: "text-indigo-700" do %> - <%= link owner.user.name, to: ~p"/users/#{owner.user.id}" %> - <% end %> - <%= table_td do %> - <%= if owner.user.slack_name == "" do %> - -- - <% else %> - <%= owner.user.slack_name %> - <% end %> - <% end %> - - <% end %> - -<% end %> diff --git a/lib/ex338_web/views/owner_view.ex b/lib/ex338_web/views/owner_view.ex deleted file mode 100644 index 637778da..00000000 --- a/lib/ex338_web/views/owner_view.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule Ex338Web.OwnerView do - use Ex338Web, :view -end