diff --git a/CHANGELOG.md b/CHANGELOG.md index 83bf1d1..f4c76d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # v0.8.4 - Improve accessibility +- Code quality improvements # v0.8.3 - Improve some styles diff --git a/lib/cannery_web.ex b/lib/cannery_web.ex index acedd85..66756e2 100644 --- a/lib/cannery_web.ex +++ b/lib/cannery_web.ex @@ -44,8 +44,7 @@ defmodule CanneryWeb do def live_view do quote do - use Phoenix.LiveView, - layout: {CanneryWeb.LayoutView, "live.html"} + use Phoenix.LiveView, layout: {CanneryWeb.LayoutView, :live} on_mount CanneryWeb.InitAssigns unquote(view_helpers()) @@ -94,7 +93,7 @@ defmodule CanneryWeb do # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc) # Import basic rendering functionality (render, render_layout, etc) - import CanneryWeb.{ErrorHelpers, Gettext, LiveHelpers, ViewHelpers} + import CanneryWeb.{ErrorHelpers, Gettext, CoreComponents, ViewHelpers} import Phoenix.{Component, View} alias CanneryWeb.Endpoint diff --git a/lib/cannery_web/components/ammo_group_card.ex b/lib/cannery_web/components/ammo_group_card.ex deleted file mode 100644 index 1af9d07..0000000 --- a/lib/cannery_web/components/ammo_group_card.ex +++ /dev/null @@ -1,103 +0,0 @@ -defmodule CanneryWeb.Components.AmmoGroupCard do - @moduledoc """ - Display card for an ammo group - """ - - use CanneryWeb, :component - alias Cannery.{Ammo, Ammo.AmmoGroup, Repo} - alias CanneryWeb.Endpoint - - attr :ammo_group, AmmoGroup, required: true - attr :show_container, :boolean, default: false - slot(:inner_block) - - def ammo_group_card(%{ammo_group: ammo_group} = assigns) do - assigns = - %{show_container: show_container} = assigns |> assign_new(:show_container, fn -> false end) - - preloads = if show_container, do: [:ammo_type, :container], else: [:ammo_type] - ammo_group = ammo_group |> Repo.preload(preloads) - - assigns = assigns |> assign(:ammo_group, ammo_group) - - ~H""" -
- <.link navigate={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} class="mb-2 link"> -

- <%= @ammo_group.ammo_type.name %> -

- - -
- - <%= gettext("Count:") %> - <%= if @ammo_group.count == 0, do: gettext("Empty"), else: @ammo_group.count %> - - - Ammo.get_original_count() != @ammo_group.count} - class="rounded-lg title text-lg" - > - <%= gettext("Original Count:") %> - <%= @ammo_group |> Ammo.get_original_count() %> - - - - <%= gettext("Notes:") %> - <%= @ammo_group.notes %> - - - - <%= gettext("Purchased on:") %> - <.date date={@ammo_group.purchased_on} /> - - - Ammo.get_last_used_shot_group()} class="rounded-lg title text-lg"> - <%= gettext("Last used on:") %> - <.date date={@ammo_group |> Ammo.get_last_used_shot_group() |> Map.get(:date)} /> - - - <%= if @ammo_group.price_paid do %> - - <%= gettext("Price paid:") %> - <%= gettext("$%{amount}", - amount: @ammo_group.price_paid |> :erlang.float_to_binary(decimals: 2) - ) %> - - - - <%= gettext("CPR:") %> - <%= gettext("$%{amount}", - amount: @ammo_group |> Ammo.get_cpr() |> :erlang.float_to_binary(decimals: 2) - ) %> - - <% end %> - - - <%= gettext("Container:") %> - - <.link - navigate={Routes.container_show_path(Endpoint, :show, @ammo_group.container)} - class="link" - > - <%= @ammo_group.container.name %> - - -
- -
Map.has_key?(:inner_block)} - class="mt-4 flex space-x-4 justify-center items-center" - > - <%= render_slot(@inner_block) %> -
-
- """ - end -end diff --git a/lib/cannery_web/components/container_card.ex b/lib/cannery_web/components/container_card.ex deleted file mode 100644 index c69637b..0000000 --- a/lib/cannery_web/components/container_card.ex +++ /dev/null @@ -1,81 +0,0 @@ -defmodule CanneryWeb.Components.ContainerCard do - @moduledoc """ - Display card for a container - """ - - use CanneryWeb, :component - import CanneryWeb.Components.TagCard - alias Cannery.{Containers, Containers.Container, Repo} - alias CanneryWeb.Endpoint - alias Phoenix.LiveView.Rendered - - attr :container, Container, required: true - slot(:tag_actions) - slot(:inner_block) - - @spec container_card(assigns :: map()) :: Rendered.t() - def container_card(%{container: container} = assigns) do - assigns = - assigns - |> assign(container: container |> Repo.preload([:tags, :ammo_groups])) - |> assign_new(:tag_actions, fn -> [] end) - - ~H""" -
-
- <.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link"> -

- <%= @container.name %> -

- - - - <%= gettext("Description:") %> - <%= @container.desc %> - - - - <%= gettext("Type:") %> - <%= @container.type %> - - - - <%= gettext("Location:") %> - <%= @container.location %> - - - <%= unless @container.ammo_groups |> Enum.empty?() do %> - - <%= gettext("Packs:") %> - <%= @container |> Containers.get_container_ammo_group_count!() %> - - - - <%= gettext("Rounds:") %> - <%= @container |> Containers.get_container_rounds!() %> - - <% end %> - -
- <.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} /> - - <%= render_slot(@tag_actions) %> -
-
- -
Map.has_key?(:inner_block)} - class="flex space-x-4 justify-center items-center" - > - <%= render_slot(@inner_block) %> -
-
- """ - end -end diff --git a/lib/cannery_web/components/container_table_component.ex b/lib/cannery_web/components/container_table_component.ex index e41d616..f4393bf 100644 --- a/lib/cannery_web/components/container_table_component.ex +++ b/lib/cannery_web/components/container_table_component.ex @@ -4,7 +4,6 @@ defmodule CanneryWeb.Components.ContainerTableComponent do """ use CanneryWeb, :live_component alias Cannery.{Accounts.User, Containers, Containers.Container, Repo} - alias CanneryWeb.Components.TagCard alias Ecto.UUID alias Phoenix.LiveView.{Rendered, Socket} @@ -135,7 +134,7 @@ defmodule CanneryWeb.Components.ContainerTableComponent do {container.tags |> Enum.map(fn %{name: name} -> name end), ~H"""
- + <.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} /> <%= render_slot(@tag_actions, @container) %>
diff --git a/lib/cannery_web/components/core_components.ex b/lib/cannery_web/components/core_components.ex new file mode 100644 index 0000000..b72945c --- /dev/null +++ b/lib/cannery_web/components/core_components.ex @@ -0,0 +1,189 @@ +defmodule CanneryWeb.CoreComponents do + @moduledoc """ + Provides core UI components. + """ + use Phoenix.Component + import CanneryWeb.{Gettext, ViewHelpers} + alias Cannery.{Accounts, Ammo, Ammo.AmmoGroup} + alias Cannery.Accounts.{Invite, Invites, User} + alias Cannery.{Containers, Containers.Container, Tags.Tag} + alias CanneryWeb.{Endpoint, HomeLive} + alias CanneryWeb.Router.Helpers, as: Routes + alias Phoenix.LiveView.{JS, Rendered} + + embed_templates "core_components/*" + + attr :title_content, :string, default: nil + attr :current_user, User, default: nil + + def topbar(assigns) + + attr :return_to, :string, required: true + slot(:inner_block) + + @doc """ + Renders a live component inside a modal. + + The rendered modal receives a `:return_to` option to properly update + the URL when the modal is closed. + + ## Examples + + <.modal return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)}> + <.live_component + module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent} + id={@<%= schema.singular %>.id || :new} + title={@page_title} + action={@live_action} + return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)} + <%= schema.singular %>: @<%= schema.singular %> + /> + + """ + def modal(assigns) + + defp hide_modal(js \\ %JS{}) do + js + |> JS.hide(to: "#modal", transition: "fade-out") + |> JS.hide(to: "#modal-bg", transition: "fade-out") + |> JS.hide(to: "#modal-content", transition: "fade-out-scale") + end + + attr :action, :string, required: true + attr :value, :boolean, required: true + attr :id, :string, default: nil + slot(:inner_block) + + @doc """ + A toggle button element that can be directed to a liveview or a + live_component's `handle_event/3`. + + ## Examples + + <.toggle_button action="my_liveview_action" value={@some_value}> + Toggle me! + + <.toggle_button action="my_live_component_action" target={@myself} value={@some_value}> + Whatever you want + + """ + def toggle_button(assigns) + + attr :container, Container, required: true + slot(:tag_actions) + slot(:inner_block) + + @spec container_card(assigns :: map()) :: Rendered.t() + def container_card(assigns) + + attr :tag, Tag, required: true + slot(:inner_block, required: true) + + def tag_card(assigns) + + attr :tag, Tag, required: true + + def simple_tag_card(assigns) + + attr :ammo_group, AmmoGroup, required: true + attr :show_container, :boolean, default: false + slot(:inner_block) + + def ammo_group_card(assigns) + + attr :user, User, required: true + slot(:inner_block, required: true) + + def user_card(assigns) + + attr :invite, Invite, required: true + attr :current_user, User, required: true + slot(:inner_block) + slot(:code_actions) + + def invite_card(%{invite: invite, current_user: current_user} = assigns) do + assigns = assigns |> assign(:use_count, Invites.get_use_count(invite, current_user)) + + ~H""" +
+

+ <%= @invite.name %> +

+ + <%= if @invite.disabled_at |> is_nil() do %> +

+ <%= if @invite.uses_left do %> + <%= gettext( + "Uses Left: %{uses_left_count}", + uses_left_count: @invite.uses_left + ) %> + <% else %> + <%= gettext("Uses Left: Unlimited") %> + <% end %> +

+ <% else %> +

+ <%= gettext("Invite Disabled") %> +

+ <% end %> + + <.qr_code + content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)} + filename={@invite.name} + /> + +

+ <%= gettext("Uses: %{uses_count}", uses_count: @use_count) %> +

+ +
+ <%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %> + <%= if @code_actions, do: render_slot(@code_actions) %> +
+ +
+ <%= render_slot(@inner_block) %> +
+
+ """ + end + + attr :content, :string, required: true + attr :filename, :string, default: "qrcode", doc: "filename without .png extension" + attr :image_class, :string, default: "w-64 h-max" + attr :width, :integer, default: 384, doc: "width of png to generate" + + @doc """ + Creates a downloadable QR Code element + """ + def qr_code(assigns) + + attr :date, :any, required: true, doc: "A `Date` struct or nil" + + @doc """ + Phoenix.Component for a element that renders the Date in the user's + local timezone with Alpine.js + """ + def date(assigns) + + attr :datetime, :any, required: true, doc: "A `DateTime` struct or nil" + + @doc """ + Phoenix.Component for a