forked from shibao/cannery
		
	use core components
This commit is contained in:
		@@ -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"""
 | 
			
		||||
    <div
 | 
			
		||||
      id={"ammo_group-#{@ammo_group.id}"}
 | 
			
		||||
      class="mx-4 my-2 px-8 py-4
 | 
			
		||||
        flex flex-col justify-center items-center
 | 
			
		||||
        border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
			
		||||
        transition-all duration-300 ease-in-out"
 | 
			
		||||
    >
 | 
			
		||||
      <.link navigate={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} class="mb-2 link">
 | 
			
		||||
        <h1 class="title text-xl title-primary-500">
 | 
			
		||||
          <%= @ammo_group.ammo_type.name %>
 | 
			
		||||
        </h1>
 | 
			
		||||
      </.link>
 | 
			
		||||
 | 
			
		||||
      <div class="flex flex-col justify-center items-center">
 | 
			
		||||
        <span class="rounded-lg title text-lg">
 | 
			
		||||
          <%= gettext("Count:") %>
 | 
			
		||||
          <%= if @ammo_group.count == 0, do: gettext("Empty"), else: @ammo_group.count %>
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <span
 | 
			
		||||
          :if={@ammo_group |> Ammo.get_original_count() != @ammo_group.count}
 | 
			
		||||
          class="rounded-lg title text-lg"
 | 
			
		||||
        >
 | 
			
		||||
          <%= gettext("Original Count:") %>
 | 
			
		||||
          <%= @ammo_group |> Ammo.get_original_count() %>
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <span :if={@ammo_group.notes} class="rounded-lg title text-lg">
 | 
			
		||||
          <%= gettext("Notes:") %>
 | 
			
		||||
          <%= @ammo_group.notes %>
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <span class="rounded-lg title text-lg">
 | 
			
		||||
          <%= gettext("Purchased on:") %>
 | 
			
		||||
          <.date date={@ammo_group.purchased_on} />
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <span :if={@ammo_group |> 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)} />
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <%= if @ammo_group.price_paid do %>
 | 
			
		||||
          <span class="rounded-lg title text-lg">
 | 
			
		||||
            <%= gettext("Price paid:") %>
 | 
			
		||||
            <%= gettext("$%{amount}",
 | 
			
		||||
              amount: @ammo_group.price_paid |> :erlang.float_to_binary(decimals: 2)
 | 
			
		||||
            ) %>
 | 
			
		||||
          </span>
 | 
			
		||||
 | 
			
		||||
          <span class="rounded-lg title text-lg">
 | 
			
		||||
            <%= gettext("CPR:") %>
 | 
			
		||||
            <%= gettext("$%{amount}",
 | 
			
		||||
              amount: @ammo_group |> Ammo.get_cpr() |> :erlang.float_to_binary(decimals: 2)
 | 
			
		||||
            ) %>
 | 
			
		||||
          </span>
 | 
			
		||||
        <% end %>
 | 
			
		||||
 | 
			
		||||
        <span :if={@show_container and @ammo_group.container} class="rounded-lg title text-lg">
 | 
			
		||||
          <%= gettext("Container:") %>
 | 
			
		||||
 | 
			
		||||
          <.link
 | 
			
		||||
            navigate={Routes.container_show_path(Endpoint, :show, @ammo_group.container)}
 | 
			
		||||
            class="link"
 | 
			
		||||
          >
 | 
			
		||||
            <%= @ammo_group.container.name %>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        :if={assigns |> Map.has_key?(:inner_block)}
 | 
			
		||||
        class="mt-4 flex space-x-4 justify-center items-center"
 | 
			
		||||
      >
 | 
			
		||||
        <%= render_slot(@inner_block) %>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -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"""
 | 
			
		||||
    <div
 | 
			
		||||
      id={"container-#{@container.id}"}
 | 
			
		||||
      class="overflow-hidden max-w-full mx-4 mb-4 px-8 py-4
 | 
			
		||||
        flex flex-col justify-center items-center space-y-4
 | 
			
		||||
        border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
			
		||||
        transition-all duration-300 ease-in-out"
 | 
			
		||||
    >
 | 
			
		||||
      <div class="max-w-full mb-4 flex flex-col justify-center items-center space-y-2">
 | 
			
		||||
        <.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link">
 | 
			
		||||
          <h1 class="px-4 py-2 rounded-lg title text-xl">
 | 
			
		||||
            <%= @container.name %>
 | 
			
		||||
          </h1>
 | 
			
		||||
        </.link>
 | 
			
		||||
 | 
			
		||||
        <span :if={@container.desc} class="rounded-lg title text-lg">
 | 
			
		||||
          <%= gettext("Description:") %>
 | 
			
		||||
          <%= @container.desc %>
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <span class="rounded-lg title text-lg">
 | 
			
		||||
          <%= gettext("Type:") %>
 | 
			
		||||
          <%= @container.type %>
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <span :if={@container.location} class="rounded-lg title text-lg">
 | 
			
		||||
          <%= gettext("Location:") %>
 | 
			
		||||
          <%= @container.location %>
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <%= unless @container.ammo_groups |> Enum.empty?() do %>
 | 
			
		||||
          <span class="rounded-lg title text-lg">
 | 
			
		||||
            <%= gettext("Packs:") %>
 | 
			
		||||
            <%= @container |> Containers.get_container_ammo_group_count!() %>
 | 
			
		||||
          </span>
 | 
			
		||||
 | 
			
		||||
          <span class="rounded-lg title text-lg">
 | 
			
		||||
            <%= gettext("Rounds:") %>
 | 
			
		||||
            <%= @container |> Containers.get_container_rounds!() %>
 | 
			
		||||
          </span>
 | 
			
		||||
        <% end %>
 | 
			
		||||
 | 
			
		||||
        <div class="flex flex-wrap justify-center items-center">
 | 
			
		||||
          <.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />
 | 
			
		||||
 | 
			
		||||
          <%= render_slot(@tag_actions) %>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        :if={assigns |> Map.has_key?(:inner_block)}
 | 
			
		||||
        class="flex space-x-4 justify-center items-center"
 | 
			
		||||
      >
 | 
			
		||||
        <%= render_slot(@inner_block) %>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -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"""
 | 
			
		||||
     <div class="flex flex-wrap justify-center items-center">
 | 
			
		||||
       <TagCard.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />
 | 
			
		||||
       <.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />
 | 
			
		||||
 | 
			
		||||
       <%= render_slot(@tag_actions, @container) %>
 | 
			
		||||
     </div>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										189
									
								
								lib/cannery_web/components/core_components.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								lib/cannery_web/components/core_components.ex
									
									
									
									
									
										Normal file
									
								
							@@ -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 %>
 | 
			
		||||
        />
 | 
			
		||||
      </.modal>
 | 
			
		||||
  """
 | 
			
		||||
  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}>
 | 
			
		||||
    <span>Toggle me!</span>
 | 
			
		||||
  </.toggle_button>
 | 
			
		||||
  <.toggle_button action="my_live_component_action" target={@myself} value={@some_value}>
 | 
			
		||||
    <span>Whatever you want</span>
 | 
			
		||||
  </.toggle_button>
 | 
			
		||||
  """
 | 
			
		||||
  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"""
 | 
			
		||||
    <div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
 | 
			
		||||
      border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
			
		||||
      transition-all duration-300 ease-in-out">
 | 
			
		||||
      <h1 class="title text-xl">
 | 
			
		||||
        <%= @invite.name %>
 | 
			
		||||
      </h1>
 | 
			
		||||
 | 
			
		||||
      <%= if @invite.disabled_at |> is_nil() do %>
 | 
			
		||||
        <h2 class="title text-md">
 | 
			
		||||
          <%= if @invite.uses_left do %>
 | 
			
		||||
            <%= gettext(
 | 
			
		||||
              "Uses Left: %{uses_left_count}",
 | 
			
		||||
              uses_left_count: @invite.uses_left
 | 
			
		||||
            ) %>
 | 
			
		||||
          <% else %>
 | 
			
		||||
            <%= gettext("Uses Left: Unlimited") %>
 | 
			
		||||
          <% end %>
 | 
			
		||||
        </h2>
 | 
			
		||||
      <% else %>
 | 
			
		||||
        <h2 class="title text-md">
 | 
			
		||||
          <%= gettext("Invite Disabled") %>
 | 
			
		||||
        </h2>
 | 
			
		||||
      <% end %>
 | 
			
		||||
 | 
			
		||||
      <.qr_code
 | 
			
		||||
        content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)}
 | 
			
		||||
        filename={@invite.name}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <h2 :if={@use_count != 0} class="title text-md">
 | 
			
		||||
        <%= gettext("Uses: %{uses_count}", uses_count: @use_count) %>
 | 
			
		||||
      </h2>
 | 
			
		||||
 | 
			
		||||
      <div class="flex flex-row flex-wrap justify-center items-center">
 | 
			
		||||
        <code
 | 
			
		||||
          id={"code-#{@invite.id}"}
 | 
			
		||||
          class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all text-gray-100 bg-primary-800"
 | 
			
		||||
          phx-no-format
 | 
			
		||||
        ><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %></code>
 | 
			
		||||
        <%= if @code_actions, do: render_slot(@code_actions) %>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div :if={@inner_block} class="flex space-x-4 justify-center items-center">
 | 
			
		||||
        <%= render_slot(@inner_block) %>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  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 <date> 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 <time> element that renders the naivedatetime in the
 | 
			
		||||
  user's local timezone with Alpine.js
 | 
			
		||||
  """
 | 
			
		||||
  def datetime(assigns)
 | 
			
		||||
 | 
			
		||||
  @spec cast_datetime(NaiveDateTime.t() | nil) :: String.t()
 | 
			
		||||
  defp cast_datetime(%NaiveDateTime{} = datetime) do
 | 
			
		||||
    datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_iso8601(:extended)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp cast_datetime(_datetime), do: ""
 | 
			
		||||
end
 | 
			
		||||
@@ -0,0 +1,74 @@
 | 
			
		||||
<div
 | 
			
		||||
  id={"ammo_group-#{@ammo_group.id}"}
 | 
			
		||||
  class="mx-4 my-2 px-8 py-4
 | 
			
		||||
    flex flex-col justify-center items-center
 | 
			
		||||
    border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
			
		||||
    transition-all duration-300 ease-in-out"
 | 
			
		||||
>
 | 
			
		||||
  <.link navigate={Routes.ammo_group_show_path(Endpoint, :show, @ammo_group)} class="mb-2 link">
 | 
			
		||||
    <h1 class="title text-xl title-primary-500">
 | 
			
		||||
      <%= @ammo_group.ammo_type.name %>
 | 
			
		||||
    </h1>
 | 
			
		||||
  </.link>
 | 
			
		||||
 | 
			
		||||
  <div class="flex flex-col justify-center items-center">
 | 
			
		||||
    <span class="rounded-lg title text-lg">
 | 
			
		||||
      <%= gettext("Count:") %>
 | 
			
		||||
      <%= if @ammo_group.count == 0, do: gettext("Empty"), else: @ammo_group.count %>
 | 
			
		||||
    </span>
 | 
			
		||||
 | 
			
		||||
    <span
 | 
			
		||||
      :if={@ammo_group |> Ammo.get_original_count() != @ammo_group.count}
 | 
			
		||||
      class="rounded-lg title text-lg"
 | 
			
		||||
    >
 | 
			
		||||
      <%= gettext("Original Count:") %>
 | 
			
		||||
      <%= @ammo_group |> Ammo.get_original_count() %>
 | 
			
		||||
    </span>
 | 
			
		||||
 | 
			
		||||
    <span :if={@ammo_group.notes} class="rounded-lg title text-lg">
 | 
			
		||||
      <%= gettext("Notes:") %>
 | 
			
		||||
      <%= @ammo_group.notes %>
 | 
			
		||||
    </span>
 | 
			
		||||
 | 
			
		||||
    <span class="rounded-lg title text-lg">
 | 
			
		||||
      <%= gettext("Purchased on:") %>
 | 
			
		||||
      <.date date={@ammo_group.purchased_on} />
 | 
			
		||||
    </span>
 | 
			
		||||
 | 
			
		||||
    <span :if={@ammo_group |> 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)} />
 | 
			
		||||
    </span>
 | 
			
		||||
 | 
			
		||||
    <%= if @ammo_group.price_paid do %>
 | 
			
		||||
      <span class="rounded-lg title text-lg">
 | 
			
		||||
        <%= gettext("Price paid:") %>
 | 
			
		||||
        <%= gettext("$%{amount}",
 | 
			
		||||
          amount: @ammo_group.price_paid |> :erlang.float_to_binary(decimals: 2)
 | 
			
		||||
        ) %>
 | 
			
		||||
      </span>
 | 
			
		||||
 | 
			
		||||
      <span class="rounded-lg title text-lg">
 | 
			
		||||
        <%= gettext("CPR:") %>
 | 
			
		||||
        <%= gettext("$%{amount}",
 | 
			
		||||
          amount: @ammo_group |> Ammo.get_cpr() |> :erlang.float_to_binary(decimals: 2)
 | 
			
		||||
        ) %>
 | 
			
		||||
      </span>
 | 
			
		||||
    <% end %>
 | 
			
		||||
 | 
			
		||||
    <span :if={@show_container and @ammo_group.container} class="rounded-lg title text-lg">
 | 
			
		||||
      <%= gettext("Container:") %>
 | 
			
		||||
 | 
			
		||||
      <.link
 | 
			
		||||
        navigate={Routes.container_show_path(Endpoint, :show, @ammo_group.container)}
 | 
			
		||||
        class="link"
 | 
			
		||||
      >
 | 
			
		||||
        <%= @ammo_group.container.name %>
 | 
			
		||||
      </.link>
 | 
			
		||||
    </span>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div :if={@inner_block} class="mt-4 flex space-x-4 justify-center items-center">
 | 
			
		||||
    <%= render_slot(@inner_block) %>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -0,0 +1,55 @@
 | 
			
		||||
<div
 | 
			
		||||
  id={"container-#{@container.id}"}
 | 
			
		||||
  class="overflow-hidden max-w-full mx-4 mb-4 px-8 py-4
 | 
			
		||||
    flex flex-col justify-center items-center space-y-4
 | 
			
		||||
    border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
			
		||||
    transition-all duration-300 ease-in-out"
 | 
			
		||||
>
 | 
			
		||||
  <div class="max-w-full mb-4 flex flex-col justify-center items-center space-y-2">
 | 
			
		||||
    <.link navigate={Routes.container_show_path(Endpoint, :show, @container)} class="link">
 | 
			
		||||
      <h1 class="px-4 py-2 rounded-lg title text-xl">
 | 
			
		||||
        <%= @container.name %>
 | 
			
		||||
      </h1>
 | 
			
		||||
    </.link>
 | 
			
		||||
 | 
			
		||||
    <span :if={@container.desc} class="rounded-lg title text-lg">
 | 
			
		||||
      <%= gettext("Description:") %>
 | 
			
		||||
      <%= @container.desc %>
 | 
			
		||||
    </span>
 | 
			
		||||
 | 
			
		||||
    <span class="rounded-lg title text-lg">
 | 
			
		||||
      <%= gettext("Type:") %>
 | 
			
		||||
      <%= @container.type %>
 | 
			
		||||
    </span>
 | 
			
		||||
 | 
			
		||||
    <span :if={@container.location} class="rounded-lg title text-lg">
 | 
			
		||||
      <%= gettext("Location:") %>
 | 
			
		||||
      <%= @container.location %>
 | 
			
		||||
    </span>
 | 
			
		||||
 | 
			
		||||
    <%= unless @container.ammo_groups |> Enum.empty?() do %>
 | 
			
		||||
      <span class="rounded-lg title text-lg">
 | 
			
		||||
        <%= gettext("Packs:") %>
 | 
			
		||||
        <%= @container |> Containers.get_container_ammo_group_count!() %>
 | 
			
		||||
      </span>
 | 
			
		||||
 | 
			
		||||
      <span class="rounded-lg title text-lg">
 | 
			
		||||
        <%= gettext("Rounds:") %>
 | 
			
		||||
        <%= @container |> Containers.get_container_rounds!() %>
 | 
			
		||||
      </span>
 | 
			
		||||
    <% end %>
 | 
			
		||||
 | 
			
		||||
    <div class="flex flex-wrap justify-center items-center">
 | 
			
		||||
      <.simple_tag_card :for={tag <- @container.tags} :if={@container.tags} tag={tag} />
 | 
			
		||||
 | 
			
		||||
      <%= if @tag_actions, do: render_slot(@tag_actions) %>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div
 | 
			
		||||
    :if={assigns |> Map.has_key?(:inner_block)}
 | 
			
		||||
    class="flex space-x-4 justify-center items-center"
 | 
			
		||||
  >
 | 
			
		||||
    <%= render_slot(@inner_block) %>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										12
									
								
								lib/cannery_web/components/core_components/date.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								lib/cannery_web/components/core_components/date.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
<time
 | 
			
		||||
  :if={@date}
 | 
			
		||||
  datetime={@date |> Date.to_iso8601(:extended)}
 | 
			
		||||
  x-data={~s<{
 | 
			
		||||
    date:
 | 
			
		||||
      Intl.DateTimeFormat([], {timeZone: 'Etc/UTC', dateStyle: 'short'})
 | 
			
		||||
        .format(new Date("#{Date.to_iso8601(@date, :extended)}"))
 | 
			
		||||
  }>}
 | 
			
		||||
  x-text="date"
 | 
			
		||||
>
 | 
			
		||||
  <%= @date |> Date.to_iso8601(:extended) %>
 | 
			
		||||
</time>
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
<time
 | 
			
		||||
  :if={@datetime}
 | 
			
		||||
  datetime={cast_datetime(@datetime)}
 | 
			
		||||
  x-data={~s/{
 | 
			
		||||
    datetime:
 | 
			
		||||
      Intl.DateTimeFormat([], {dateStyle: 'short', timeStyle: 'long'})
 | 
			
		||||
        .format(new Date("#{cast_datetime(@datetime)}"))
 | 
			
		||||
  }/}
 | 
			
		||||
  x-text="datetime"
 | 
			
		||||
>
 | 
			
		||||
  <%= cast_datetime(@datetime) %>
 | 
			
		||||
</time>
 | 
			
		||||
							
								
								
									
										42
									
								
								lib/cannery_web/components/core_components/modal.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								lib/cannery_web/components/core_components/modal.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
<.link
 | 
			
		||||
  patch={@return_to}
 | 
			
		||||
  id="modal-bg"
 | 
			
		||||
  class="fade-in fixed z-10 left-0 top-0
 | 
			
		||||
    w-full h-full overflow-hidden
 | 
			
		||||
    p-8 flex flex-col justify-center items-center cursor-auto"
 | 
			
		||||
  style="background-color: rgba(0,0,0,0.4);"
 | 
			
		||||
  phx-remove={hide_modal()}
 | 
			
		||||
>
 | 
			
		||||
  <span class="hidden"></span>
 | 
			
		||||
</.link>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
  id="modal"
 | 
			
		||||
  class="fixed z-10 left-0 top-0 pointer-events-none
 | 
			
		||||
    w-full h-full overflow-hidden
 | 
			
		||||
    p-4 sm:p-8 flex flex-col justify-center items-center"
 | 
			
		||||
>
 | 
			
		||||
  <div
 | 
			
		||||
    id="modal-content"
 | 
			
		||||
    class="fade-in-scale w-full max-w-3xl relative
 | 
			
		||||
      pointer-events-auto overflow-hidden
 | 
			
		||||
      px-8 py-4 sm:py-8
 | 
			
		||||
      flex flex-col justify-start items-center
 | 
			
		||||
      bg-white border-2 rounded-lg"
 | 
			
		||||
  >
 | 
			
		||||
    <.link
 | 
			
		||||
      patch={@return_to}
 | 
			
		||||
      id="close"
 | 
			
		||||
      class="absolute top-8 right-10
 | 
			
		||||
        text-gray-500 hover:text-gray-800
 | 
			
		||||
        transition-all duration-500 ease-in-out"
 | 
			
		||||
      phx-remove={hide_modal()}
 | 
			
		||||
    >
 | 
			
		||||
      <i class="fa-fw fa-lg fas fa-times"></i>
 | 
			
		||||
    </.link>
 | 
			
		||||
 | 
			
		||||
    <div class="overflow-x-hidden overflow-y-auto w-full p-8 flex flex-col space-y-4 justify-start items-center">
 | 
			
		||||
      <%= render_slot(@inner_block) %>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
<.link href={qr_code_image(@content)} download={@filename <> ".png"}>
 | 
			
		||||
  <img class={@image_class} alt={@filename} src={qr_code_image(@content)} />
 | 
			
		||||
</.link>
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
<h1
 | 
			
		||||
  class="inline-block break-all mx-2 my-1 px-4 py-2 rounded-lg title text-xl"
 | 
			
		||||
  style={"color: #{@tag.text_color}; background-color: #{@tag.bg_color}"}
 | 
			
		||||
>
 | 
			
		||||
  <%= @tag.name %>
 | 
			
		||||
</h1>
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
<div
 | 
			
		||||
  id={"tag-#{@tag.id}"}
 | 
			
		||||
  class="mx-4 mb-4 px-8 py-4 space-x-4 flex justify-center items-center
 | 
			
		||||
    border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
			
		||||
    transition-all duration-300 ease-in-out"
 | 
			
		||||
>
 | 
			
		||||
  <.simple_tag_card tag={@tag} />
 | 
			
		||||
  <%= render_slot(@inner_block) %>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
<label for={@id || @action} class="inline-flex relative items-center cursor-pointer">
 | 
			
		||||
  <input
 | 
			
		||||
    id={@id || @action}
 | 
			
		||||
    type="checkbox"
 | 
			
		||||
    value={@value}
 | 
			
		||||
    checked={@value}
 | 
			
		||||
    class="sr-only peer"
 | 
			
		||||
    aria-labelledby={"#{@id || @action}-label"}
 | 
			
		||||
    {
 | 
			
		||||
      if assigns |> Map.has_key?(:target),
 | 
			
		||||
        do: %{"phx-click": @action, "phx-value-value": @value, "phx-target": @target},
 | 
			
		||||
        else: %{"phx-click": @action, "phx-value-value": @value}
 | 
			
		||||
    }
 | 
			
		||||
  />
 | 
			
		||||
  <div class="w-11 h-6 bg-gray-300 rounded-full peer
 | 
			
		||||
    peer-focus:ring-4 peer-focus:ring-teal-300 dark:peer-focus:ring-teal-800
 | 
			
		||||
    peer-checked:bg-gray-600
 | 
			
		||||
    peer-checked:after:translate-x-full peer-checked:after:border-white
 | 
			
		||||
    after:content-[''] after:absolute after:top-1 after:left-[2px] after:bg-white after:border-gray-300
 | 
			
		||||
    after:border after:rounded-full after:h-5 after:w-5
 | 
			
		||||
    after:transition-all after:duration-250 after:ease-in-out
 | 
			
		||||
    transition-colors duration-250 ease-in-out">
 | 
			
		||||
  </div>
 | 
			
		||||
  <span
 | 
			
		||||
    id={"#{@id || @action}-label"}
 | 
			
		||||
    class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
 | 
			
		||||
  >
 | 
			
		||||
    <%= render_slot(@inner_block) %>
 | 
			
		||||
  </span>
 | 
			
		||||
</label>
 | 
			
		||||
							
								
								
									
										128
									
								
								lib/cannery_web/components/core_components/topbar.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								lib/cannery_web/components/core_components/topbar.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
			
		||||
<nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-400">
 | 
			
		||||
  <div class="flex flex-col sm:flex-row justify-between items-center">
 | 
			
		||||
    <div class="mb-4 sm:mb-0 sm:mr-8 flex flex-row justify-start items-center space-x-2">
 | 
			
		||||
      <.link
 | 
			
		||||
        navigate={Routes.live_path(Endpoint, HomeLive)}
 | 
			
		||||
        class="inline mx-2 my-1 leading-5 text-xl text-white"
 | 
			
		||||
      >
 | 
			
		||||
        <img
 | 
			
		||||
          src={Routes.static_path(Endpoint, "/images/cannery.svg")}
 | 
			
		||||
          alt={gettext("Cannery logo")}
 | 
			
		||||
          class="inline-block h-8 mx-1"
 | 
			
		||||
        />
 | 
			
		||||
        <h1 class="inline hover:underline">Cannery</h1>
 | 
			
		||||
      </.link>
 | 
			
		||||
 | 
			
		||||
      <%= if @title_content do %>
 | 
			
		||||
        <span class="mx-2 my-1">
 | 
			
		||||
          |
 | 
			
		||||
        </span>
 | 
			
		||||
        <%= @title_content %>
 | 
			
		||||
      <% end %>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <hr class="mb-2 sm:hidden hr-light" />
 | 
			
		||||
 | 
			
		||||
    <ul class="flex flex-row flex-wrap justify-center items-center
 | 
			
		||||
      text-lg text-white text-ellipsis">
 | 
			
		||||
      <%= if @current_user do %>
 | 
			
		||||
        <li class="mx-2 my-1">
 | 
			
		||||
          <.link
 | 
			
		||||
            navigate={Routes.tag_index_path(Endpoint, :index)}
 | 
			
		||||
            class="text-white hover:underline"
 | 
			
		||||
          >
 | 
			
		||||
            <%= gettext("Tags") %>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="mx-2 my-1">
 | 
			
		||||
          <.link
 | 
			
		||||
            navigate={Routes.container_index_path(Endpoint, :index)}
 | 
			
		||||
            class="text-white hover:underline"
 | 
			
		||||
          >
 | 
			
		||||
            <%= gettext("Containers") %>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="mx-2 my-1">
 | 
			
		||||
          <.link
 | 
			
		||||
            navigate={Routes.ammo_type_index_path(Endpoint, :index)}
 | 
			
		||||
            class="text-white hover:underline"
 | 
			
		||||
          >
 | 
			
		||||
            <%= gettext("Catalog") %>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="mx-2 my-1">
 | 
			
		||||
          <.link
 | 
			
		||||
            navigate={Routes.ammo_group_index_path(Endpoint, :index)}
 | 
			
		||||
            class="text-white hover:underline"
 | 
			
		||||
          >
 | 
			
		||||
            <%= gettext("Ammo") %>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="mx-2 my-1">
 | 
			
		||||
          <.link
 | 
			
		||||
            navigate={Routes.range_index_path(Endpoint, :index)}
 | 
			
		||||
            class="text-white hover:underline"
 | 
			
		||||
          >
 | 
			
		||||
            <%= gettext("Range") %>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li :if={@current_user |> Accounts.is_already_admin?()} class="mx-2 my-1">
 | 
			
		||||
          <.link
 | 
			
		||||
            navigate={Routes.invite_index_path(Endpoint, :index)}
 | 
			
		||||
            class="text-white hover:underline"
 | 
			
		||||
          >
 | 
			
		||||
            <%= gettext("Invites") %>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="mx-2 my-1">
 | 
			
		||||
          <.link
 | 
			
		||||
            href={Routes.user_settings_path(Endpoint, :edit)}
 | 
			
		||||
            class="text-white hover:underline truncate"
 | 
			
		||||
          >
 | 
			
		||||
            <%= @current_user.email %>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="mx-2 my-1">
 | 
			
		||||
          <.link
 | 
			
		||||
            href={Routes.user_session_path(Endpoint, :delete)}
 | 
			
		||||
            method="delete"
 | 
			
		||||
            data-confirm={dgettext("prompts", "Are you sure you want to log out?")}
 | 
			
		||||
          >
 | 
			
		||||
            <i class="fas fa-sign-out-alt"></i>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li
 | 
			
		||||
          :if={
 | 
			
		||||
            @current_user |> Accounts.is_already_admin?() and
 | 
			
		||||
              function_exported?(Routes, :live_dashboard_path, 2)
 | 
			
		||||
          }
 | 
			
		||||
          class="mx-2 my-1"
 | 
			
		||||
        >
 | 
			
		||||
          <.link
 | 
			
		||||
            navigate={Routes.live_dashboard_path(Endpoint, :home)}
 | 
			
		||||
            class="text-white hover:underline"
 | 
			
		||||
          >
 | 
			
		||||
            <i class="fas fa-gauge"></i>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
      <% else %>
 | 
			
		||||
        <li :if={Accounts.allow_registration?()} class="mx-2 my-1">
 | 
			
		||||
          <.link
 | 
			
		||||
            href={Routes.user_registration_path(Endpoint, :new)}
 | 
			
		||||
            class="text-white hover:underline truncate"
 | 
			
		||||
          >
 | 
			
		||||
            <%= dgettext("actions", "Register") %>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="mx-2 my-1">
 | 
			
		||||
          <.link
 | 
			
		||||
            href={Routes.user_session_path(Endpoint, :new)}
 | 
			
		||||
            class="text-white hover:underline truncate"
 | 
			
		||||
          >
 | 
			
		||||
            <%= dgettext("actions", "Log in") %>
 | 
			
		||||
          </.link>
 | 
			
		||||
        </li>
 | 
			
		||||
      <% end %>
 | 
			
		||||
    </ul>
 | 
			
		||||
  </div>
 | 
			
		||||
</nav>
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
<div
 | 
			
		||||
  id={"user-#{@user.id}"}
 | 
			
		||||
  class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center text-center
 | 
			
		||||
    border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
			
		||||
    transition-all duration-300 ease-in-out"
 | 
			
		||||
>
 | 
			
		||||
  <h1 class="px-4 py-2 rounded-lg title text-xl break-all">
 | 
			
		||||
    <%= @user.email %>
 | 
			
		||||
  </h1>
 | 
			
		||||
 | 
			
		||||
  <h3 class="px-4 py-2 rounded-lg title text-lg">
 | 
			
		||||
    <p>
 | 
			
		||||
      <%= if @user.confirmed_at do %>
 | 
			
		||||
        <%= gettext(
 | 
			
		||||
          "User was confirmed at%{confirmed_datetime}",
 | 
			
		||||
          confirmed_datetime: ""
 | 
			
		||||
        ) %>
 | 
			
		||||
        <.datetime datetime={@user.confirmed_at} />
 | 
			
		||||
      <% else %>
 | 
			
		||||
        <%= gettext("Email unconfirmed") %>
 | 
			
		||||
      <% end %>
 | 
			
		||||
    </p>
 | 
			
		||||
 | 
			
		||||
    <p>
 | 
			
		||||
      <%= gettext(
 | 
			
		||||
        "User registered on%{registered_datetime}",
 | 
			
		||||
        registered_datetime: ""
 | 
			
		||||
      ) %>
 | 
			
		||||
      <.datetime datetime={@user.inserted_at} />
 | 
			
		||||
    </p>
 | 
			
		||||
  </h3>
 | 
			
		||||
 | 
			
		||||
  <div :if={@inner_block} class="px-4 py-2 flex space-x-4 justify-center items-center">
 | 
			
		||||
    <%= render_slot(@inner_block) %>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,70 +0,0 @@
 | 
			
		||||
defmodule CanneryWeb.Components.InviteCard do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Display card for an invite
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use CanneryWeb, :component
 | 
			
		||||
  alias Cannery.Accounts.{Invite, Invites, User}
 | 
			
		||||
  alias CanneryWeb.Endpoint
 | 
			
		||||
 | 
			
		||||
  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))
 | 
			
		||||
      |> assign_new(:code_actions, fn -> [] end)
 | 
			
		||||
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
 | 
			
		||||
      border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
			
		||||
      transition-all duration-300 ease-in-out">
 | 
			
		||||
      <h1 class="title text-xl">
 | 
			
		||||
        <%= @invite.name %>
 | 
			
		||||
      </h1>
 | 
			
		||||
 | 
			
		||||
      <%= if @invite.disabled_at |> is_nil() do %>
 | 
			
		||||
        <h2 class="title text-md">
 | 
			
		||||
          <%= if @invite.uses_left do %>
 | 
			
		||||
            <%= gettext(
 | 
			
		||||
              "Uses Left: %{uses_left_count}",
 | 
			
		||||
              uses_left_count: @invite.uses_left
 | 
			
		||||
            ) %>
 | 
			
		||||
          <% else %>
 | 
			
		||||
            <%= gettext("Uses Left: Unlimited") %>
 | 
			
		||||
          <% end %>
 | 
			
		||||
        </h2>
 | 
			
		||||
      <% else %>
 | 
			
		||||
        <h2 class="title text-md">
 | 
			
		||||
          <%= gettext("Invite Disabled") %>
 | 
			
		||||
        </h2>
 | 
			
		||||
      <% end %>
 | 
			
		||||
 | 
			
		||||
      <.qr_code
 | 
			
		||||
        content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)}
 | 
			
		||||
        filename={@invite.name}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <h2 :if={@use_count != 0} class="title text-md">
 | 
			
		||||
        <%= gettext("Uses: %{uses_count}", uses_count: @use_count) %>
 | 
			
		||||
      </h2>
 | 
			
		||||
 | 
			
		||||
      <div class="flex flex-row flex-wrap justify-center items-center">
 | 
			
		||||
        <code
 | 
			
		||||
          id={"code-#{@invite.id}"}
 | 
			
		||||
          class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all text-gray-100 bg-primary-800"
 | 
			
		||||
          phx-no-format
 | 
			
		||||
        ><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %></code>
 | 
			
		||||
        <%= render_slot(@code_actions) %>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div :if={@inner_block} class="flex space-x-4 justify-center items-center">
 | 
			
		||||
        <%= render_slot(@inner_block) %>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,38 +0,0 @@
 | 
			
		||||
defmodule CanneryWeb.Components.TagCard do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Display card for a tag
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use CanneryWeb, :component
 | 
			
		||||
  alias Cannery.Tags.Tag
 | 
			
		||||
 | 
			
		||||
  attr :tag, Tag, required: true
 | 
			
		||||
  slot(:inner_block, required: true)
 | 
			
		||||
 | 
			
		||||
  def tag_card(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div
 | 
			
		||||
      id={"tag-#{@tag.id}"}
 | 
			
		||||
      class="mx-4 mb-4 px-8 py-4 space-x-4 flex justify-center items-center
 | 
			
		||||
        border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
			
		||||
        transition-all duration-300 ease-in-out"
 | 
			
		||||
    >
 | 
			
		||||
      <.simple_tag_card tag={@tag} />
 | 
			
		||||
      <%= render_slot(@inner_block) %>
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  attr :tag, Tag, required: true
 | 
			
		||||
 | 
			
		||||
  def simple_tag_card(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <h1
 | 
			
		||||
      class="inline-block break-all mx-2 my-1 px-4 py-2 rounded-lg title text-xl"
 | 
			
		||||
      style={"color: #{@tag.text_color}; background-color: #{@tag.bg_color}"}
 | 
			
		||||
    >
 | 
			
		||||
      <%= @tag.name %>
 | 
			
		||||
    </h1>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,146 +0,0 @@
 | 
			
		||||
defmodule CanneryWeb.Components.Topbar do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Component that renders a topbar with user functions/links
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use CanneryWeb, :component
 | 
			
		||||
 | 
			
		||||
  alias Cannery.Accounts
 | 
			
		||||
  alias CanneryWeb.HomeLive
 | 
			
		||||
 | 
			
		||||
  def topbar(assigns) do
 | 
			
		||||
    assigns =
 | 
			
		||||
      %{results: [], title_content: nil, flash: nil, current_user: nil} |> Map.merge(assigns)
 | 
			
		||||
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-400">
 | 
			
		||||
      <div class="flex flex-col sm:flex-row justify-between items-center">
 | 
			
		||||
        <div class="mb-4 sm:mb-0 sm:mr-8 flex flex-row justify-start items-center space-x-2">
 | 
			
		||||
          <.link
 | 
			
		||||
            navigate={Routes.live_path(Endpoint, HomeLive)}
 | 
			
		||||
            class="inline mx-2 my-1 leading-5 text-xl text-white"
 | 
			
		||||
          >
 | 
			
		||||
            <img
 | 
			
		||||
              src={Routes.static_path(Endpoint, "/images/cannery.svg")}
 | 
			
		||||
              alt={gettext("Cannery logo")}
 | 
			
		||||
              class="inline-block h-8 mx-1"
 | 
			
		||||
            />
 | 
			
		||||
            <h1 class="inline hover:underline">Cannery</h1>
 | 
			
		||||
          </.link>
 | 
			
		||||
 | 
			
		||||
          <%= if @title_content do %>
 | 
			
		||||
            <span class="mx-2 my-1">
 | 
			
		||||
              |
 | 
			
		||||
            </span>
 | 
			
		||||
            <%= @title_content %>
 | 
			
		||||
          <% end %>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <hr class="mb-2 sm:hidden hr-light" />
 | 
			
		||||
 | 
			
		||||
        <ul class="flex flex-row flex-wrap justify-center items-center
 | 
			
		||||
          text-lg text-white text-ellipsis">
 | 
			
		||||
          <%= if @current_user do %>
 | 
			
		||||
            <li class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                navigate={Routes.tag_index_path(Endpoint, :index)}
 | 
			
		||||
                class="text-primary-600 text-white hover:underline"
 | 
			
		||||
              >
 | 
			
		||||
                <%= gettext("Tags") %>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                navigate={Routes.container_index_path(Endpoint, :index)}
 | 
			
		||||
                class="text-primary-600 text-white hover:underline"
 | 
			
		||||
              >
 | 
			
		||||
                <%= gettext("Containers") %>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                navigate={Routes.ammo_type_index_path(Endpoint, :index)}
 | 
			
		||||
                class="text-primary-600 text-white hover:underline"
 | 
			
		||||
              >
 | 
			
		||||
                <%= gettext("Catalog") %>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                navigate={Routes.ammo_group_index_path(Endpoint, :index)}
 | 
			
		||||
                class="text-primary-600 text-white hover:underline"
 | 
			
		||||
              >
 | 
			
		||||
                <%= gettext("Ammo") %>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                navigate={Routes.range_index_path(Endpoint, :index)}
 | 
			
		||||
                class="text-primary-600 text-white hover:underline"
 | 
			
		||||
              >
 | 
			
		||||
                <%= gettext("Range") %>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li :if={@current_user |> Accounts.is_already_admin?()} class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                navigate={Routes.invite_index_path(Endpoint, :index)}
 | 
			
		||||
                class="text-primary-600 text-white hover:underline"
 | 
			
		||||
              >
 | 
			
		||||
                <%= gettext("Invites") %>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                href={Routes.user_settings_path(Endpoint, :edit)}
 | 
			
		||||
                class="text-primary-600 text-white hover:underline truncate"
 | 
			
		||||
              >
 | 
			
		||||
                <%= @current_user.email %>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                href={Routes.user_session_path(Endpoint, :delete)}
 | 
			
		||||
                method="delete"
 | 
			
		||||
                data-confirm={dgettext("prompts", "Are you sure you want to log out?")}
 | 
			
		||||
              >
 | 
			
		||||
                <i class="fas fa-sign-out-alt"></i>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li
 | 
			
		||||
              :if={
 | 
			
		||||
                @current_user |> Accounts.is_already_admin?() and
 | 
			
		||||
                  function_exported?(Routes, :live_dashboard_path, 2)
 | 
			
		||||
              }
 | 
			
		||||
              class="mx-2 my-1"
 | 
			
		||||
            >
 | 
			
		||||
              <.link
 | 
			
		||||
                navigate={Routes.live_dashboard_path(Endpoint, :home)}
 | 
			
		||||
                class="text-white text-white hover:underline"
 | 
			
		||||
              >
 | 
			
		||||
                <i class="fas fa-gauge"></i>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
          <% else %>
 | 
			
		||||
            <li :if={Accounts.allow_registration?()} class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                href={Routes.user_registration_path(Endpoint, :new)}
 | 
			
		||||
                class="text-white hover:underline truncate"
 | 
			
		||||
              >
 | 
			
		||||
                <%= dgettext("actions", "Register") %>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="mx-2 my-1">
 | 
			
		||||
              <.link
 | 
			
		||||
                href={Routes.user_session_path(Endpoint, :new)}
 | 
			
		||||
                class="text-white hover:underline truncate"
 | 
			
		||||
              >
 | 
			
		||||
                <%= dgettext("actions", "Log in") %>
 | 
			
		||||
              </.link>
 | 
			
		||||
            </li>
 | 
			
		||||
          <% end %>
 | 
			
		||||
        </ul>
 | 
			
		||||
      </div>
 | 
			
		||||
    </nav>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
defmodule CanneryWeb.Components.UserCard do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Display card for a user
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use CanneryWeb, :component
 | 
			
		||||
  alias Cannery.Accounts.User
 | 
			
		||||
 | 
			
		||||
  attr :user, User, required: true
 | 
			
		||||
  slot(:inner_block, required: true)
 | 
			
		||||
 | 
			
		||||
  def user_card(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div
 | 
			
		||||
      id={"user-#{@user.id}"}
 | 
			
		||||
      class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center text-center
 | 
			
		||||
        border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
			
		||||
        transition-all duration-300 ease-in-out"
 | 
			
		||||
    >
 | 
			
		||||
      <h1 class="px-4 py-2 rounded-lg title text-xl break-all">
 | 
			
		||||
        <%= @user.email %>
 | 
			
		||||
      </h1>
 | 
			
		||||
 | 
			
		||||
      <h3 class="px-4 py-2 rounded-lg title text-lg">
 | 
			
		||||
        <p>
 | 
			
		||||
          <%= if @user.confirmed_at do %>
 | 
			
		||||
            <%= gettext(
 | 
			
		||||
              "User was confirmed at%{confirmed_datetime}",
 | 
			
		||||
              confirmed_datetime: ""
 | 
			
		||||
            ) %>
 | 
			
		||||
            <.datetime datetime={@user.confirmed_at} />
 | 
			
		||||
          <% else %>
 | 
			
		||||
            <%= gettext("Email unconfirmed") %>
 | 
			
		||||
          <% end %>
 | 
			
		||||
        </p>
 | 
			
		||||
 | 
			
		||||
        <p>
 | 
			
		||||
          <%= gettext(
 | 
			
		||||
            "User registered on%{registered_datetime}",
 | 
			
		||||
            registered_datetime: ""
 | 
			
		||||
          ) %>
 | 
			
		||||
          <.datetime datetime={@user.inserted_at} />
 | 
			
		||||
        </p>
 | 
			
		||||
      </h3>
 | 
			
		||||
 | 
			
		||||
      <div :if={@inner_block} class="px-4 py-2 flex space-x-4 justify-center items-center">
 | 
			
		||||
        <%= render_slot(@inner_block) %>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -45,7 +45,8 @@
 | 
			
		||||
  <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-xl">
 | 
			
		||||
    <.form
 | 
			
		||||
      :let={f}
 | 
			
		||||
      for={:search}
 | 
			
		||||
      for={%{}}
 | 
			
		||||
      as={:search}
 | 
			
		||||
      phx-change="search"
 | 
			
		||||
      phx-submit="search"
 | 
			
		||||
      class="grow self-stretch flex flex-col items-stretch"
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ defmodule CanneryWeb.AmmoGroupLive.Show do
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use CanneryWeb, :live_view
 | 
			
		||||
  import CanneryWeb.Components.ContainerCard
 | 
			
		||||
  alias Cannery.{ActivityLog, ActivityLog.ShotGroup, Ammo, Ammo.AmmoGroup, Repo}
 | 
			
		||||
  alias CanneryWeb.Endpoint
 | 
			
		||||
  alias Phoenix.LiveView.Socket
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,8 @@
 | 
			
		||||
    <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-xl">
 | 
			
		||||
      <.form
 | 
			
		||||
        :let={f}
 | 
			
		||||
        for={:search}
 | 
			
		||||
        for={%{}}
 | 
			
		||||
        as={:search}
 | 
			
		||||
        phx-change="search"
 | 
			
		||||
        phx-submit="search"
 | 
			
		||||
        class="grow self-stretch flex flex-col items-stretch"
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ defmodule CanneryWeb.AmmoTypeLive.Show do
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use CanneryWeb, :live_view
 | 
			
		||||
  import CanneryWeb.Components.AmmoGroupCard
 | 
			
		||||
  alias Cannery.{Ammo, Ammo.AmmoType}
 | 
			
		||||
  alias CanneryWeb.Endpoint
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,8 @@
 | 
			
		||||
 | 
			
		||||
    <.form
 | 
			
		||||
      :let={f}
 | 
			
		||||
      for={:tag}
 | 
			
		||||
      for={%{}}
 | 
			
		||||
      as={:tag}
 | 
			
		||||
      id="add-tag-to-container-form"
 | 
			
		||||
      class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
			
		||||
      phx-target={@myself}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ defmodule CanneryWeb.ContainerLive.Index do
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use CanneryWeb, :live_view
 | 
			
		||||
  import CanneryWeb.Components.ContainerCard
 | 
			
		||||
  alias Cannery.{Containers, Containers.Container, Repo}
 | 
			
		||||
  alias Ecto.Changeset
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,8 @@
 | 
			
		||||
    <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-xl">
 | 
			
		||||
      <.form
 | 
			
		||||
        :let={f}
 | 
			
		||||
        for={:search}
 | 
			
		||||
        for={%{}}
 | 
			
		||||
        as={:search}
 | 
			
		||||
        phx-change="search"
 | 
			
		||||
        phx-submit="search"
 | 
			
		||||
        class="grow self-stretch flex flex-col items-stretch"
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ defmodule CanneryWeb.ContainerLive.Show do
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use CanneryWeb, :live_view
 | 
			
		||||
  import CanneryWeb.Components.{AmmoGroupCard, TagCard}
 | 
			
		||||
  alias Cannery.{Accounts.User, Ammo, Containers, Containers.Container, Repo, Tags}
 | 
			
		||||
  alias CanneryWeb.Endpoint
 | 
			
		||||
  alias Ecto.Changeset
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ defmodule CanneryWeb.InviteLive.Index do
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use CanneryWeb, :live_view
 | 
			
		||||
  import CanneryWeb.Components.{InviteCard, UserCard}
 | 
			
		||||
  alias Cannery.Accounts
 | 
			
		||||
  alias Cannery.Accounts.{Invite, Invites}
 | 
			
		||||
  alias CanneryWeb.HomeLive
 | 
			
		||||
 
 | 
			
		||||
@@ -1,136 +0,0 @@
 | 
			
		||||
defmodule CanneryWeb.LiveHelpers do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Contains common helper functions for liveviews
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use Phoenix.Component
 | 
			
		||||
  alias Phoenix.LiveView.JS
 | 
			
		||||
 | 
			
		||||
  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 %>
 | 
			
		||||
        />
 | 
			
		||||
      </.modal>
 | 
			
		||||
  """
 | 
			
		||||
  def modal(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <.link
 | 
			
		||||
      patch={@return_to}
 | 
			
		||||
      id="modal-bg"
 | 
			
		||||
      class="fade-in fixed z-10 left-0 top-0
 | 
			
		||||
        w-full h-full overflow-hidden
 | 
			
		||||
        p-8 flex flex-col justify-center items-center cursor-auto"
 | 
			
		||||
      style="background-color: rgba(0,0,0,0.4);"
 | 
			
		||||
      phx-remove={hide_modal()}
 | 
			
		||||
    >
 | 
			
		||||
      <span class="hidden"></span>
 | 
			
		||||
    </.link>
 | 
			
		||||
 | 
			
		||||
    <div
 | 
			
		||||
      id="modal"
 | 
			
		||||
      class="fixed z-10 left-0 top-0 pointer-events-none
 | 
			
		||||
        w-full h-full overflow-hidden
 | 
			
		||||
        p-4 sm:p-8 flex flex-col justify-center items-center"
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        id="modal-content"
 | 
			
		||||
        class="fade-in-scale w-full max-w-3xl relative
 | 
			
		||||
          pointer-events-auto overflow-hidden
 | 
			
		||||
          px-8 py-4 sm:py-8
 | 
			
		||||
          flex flex-col justify-start items-center
 | 
			
		||||
          bg-white border-2 rounded-lg"
 | 
			
		||||
      >
 | 
			
		||||
        <.link
 | 
			
		||||
          patch={@return_to}
 | 
			
		||||
          id="close"
 | 
			
		||||
          class="absolute top-8 right-10
 | 
			
		||||
            text-gray-500 hover:text-gray-800
 | 
			
		||||
            transition-all duration-500 ease-in-out"
 | 
			
		||||
          phx-remove={hide_modal()}
 | 
			
		||||
        >
 | 
			
		||||
          <i class="fa-fw fa-lg fas fa-times"></i>
 | 
			
		||||
        </.link>
 | 
			
		||||
 | 
			
		||||
        <div class="overflow-x-hidden overflow-y-auto w-full p-8 flex flex-col space-y-4 justify-start items-center">
 | 
			
		||||
          <%= render_slot(@inner_block) %>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
  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}>
 | 
			
		||||
    <span>Toggle me!</span>
 | 
			
		||||
  </.toggle_button>
 | 
			
		||||
  <.toggle_button action="my_live_component_action" target={@myself} value={@some_value}>
 | 
			
		||||
    <span>Whatever you want</span>
 | 
			
		||||
  </.toggle_button>
 | 
			
		||||
  """
 | 
			
		||||
  def toggle_button(assigns) do
 | 
			
		||||
    assigns = assigns |> assign_new(:id, fn -> assigns.action end)
 | 
			
		||||
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <label for={@id} class="inline-flex relative items-center cursor-pointer">
 | 
			
		||||
      <input
 | 
			
		||||
        id={@id}
 | 
			
		||||
        type="checkbox"
 | 
			
		||||
        value={@value}
 | 
			
		||||
        checked={@value}
 | 
			
		||||
        class="sr-only peer"
 | 
			
		||||
        aria-labelledby={"#{@id}-label"}
 | 
			
		||||
        {
 | 
			
		||||
          if assigns |> Map.has_key?(:target),
 | 
			
		||||
            do: %{"phx-click": @action, "phx-value-value": @value, "phx-target": @target},
 | 
			
		||||
            else: %{"phx-click": @action, "phx-value-value": @value}
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
      <div class="w-11 h-6 bg-gray-300 rounded-full peer
 | 
			
		||||
        peer-focus:ring-4 peer-focus:ring-teal-300 dark:peer-focus:ring-teal-800
 | 
			
		||||
        peer-checked:bg-gray-600
 | 
			
		||||
        peer-checked:after:translate-x-full peer-checked:after:border-white
 | 
			
		||||
        after:content-[''] after:absolute after:top-1 after:left-[2px] after:bg-white after:border-gray-300
 | 
			
		||||
        after:border after:rounded-full after:h-5 after:w-5
 | 
			
		||||
        after:transition-all after:duration-250 after:ease-in-out
 | 
			
		||||
        transition-colors duration-250 ease-in-out">
 | 
			
		||||
      </div>
 | 
			
		||||
      <span id={"#{@id}-label"} class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">
 | 
			
		||||
        <%= render_slot(@inner_block) %>
 | 
			
		||||
      </span>
 | 
			
		||||
    </label>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -4,7 +4,6 @@ defmodule CanneryWeb.RangeLive.Index do
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use CanneryWeb, :live_view
 | 
			
		||||
  import CanneryWeb.Components.AmmoGroupCard
 | 
			
		||||
  alias Cannery.{ActivityLog, ActivityLog.ShotGroup, Ammo, Repo}
 | 
			
		||||
  alias CanneryWeb.Endpoint
 | 
			
		||||
  alias Phoenix.LiveView.Socket
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,8 @@
 | 
			
		||||
    <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-xl">
 | 
			
		||||
      <.form
 | 
			
		||||
        :let={f}
 | 
			
		||||
        for={:search}
 | 
			
		||||
        for={%{}}
 | 
			
		||||
        as={:search}
 | 
			
		||||
        phx-change="search"
 | 
			
		||||
        phx-submit="search"
 | 
			
		||||
        class="grow self-stretch flex flex-col items-stretch"
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ defmodule CanneryWeb.TagLive.Index do
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use CanneryWeb, :live_view
 | 
			
		||||
  import CanneryWeb.Components.TagCard
 | 
			
		||||
  alias Cannery.{Tags, Tags.Tag}
 | 
			
		||||
  alias CanneryWeb.ViewHelpers
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,8 @@
 | 
			
		||||
  <div class="w-full flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4 max-w-xl">
 | 
			
		||||
    <.form
 | 
			
		||||
      :let={f}
 | 
			
		||||
      for={:search}
 | 
			
		||||
      for={%{}}
 | 
			
		||||
      as={:search}
 | 
			
		||||
      phx-change="search"
 | 
			
		||||
      phx-submit="search"
 | 
			
		||||
      class="grow self-stretch flex flex-col items-stretch"
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,8 @@
 | 
			
		||||
 | 
			
		||||
  <.form
 | 
			
		||||
    :let={f}
 | 
			
		||||
    for={:user}
 | 
			
		||||
    for={%{}}
 | 
			
		||||
    as={:user}
 | 
			
		||||
    action={Routes.user_confirmation_path(@conn, :create)}
 | 
			
		||||
    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
			
		||||
  >
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,8 @@
 | 
			
		||||
 | 
			
		||||
  <.form
 | 
			
		||||
    :let={f}
 | 
			
		||||
    for={:user}
 | 
			
		||||
    for={%{}}
 | 
			
		||||
    as={:user}
 | 
			
		||||
    action={Routes.user_reset_password_path(@conn, :create)}
 | 
			
		||||
    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
			
		||||
  >
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
defmodule CanneryWeb.ErrorView do
 | 
			
		||||
  use CanneryWeb, :view
 | 
			
		||||
  import CanneryWeb.Components.Topbar
 | 
			
		||||
  alias CanneryWeb.HomeLive
 | 
			
		||||
 | 
			
		||||
  def template_not_found(error_path, _assigns) do
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
defmodule CanneryWeb.LayoutView do
 | 
			
		||||
  use CanneryWeb, :view
 | 
			
		||||
  import CanneryWeb.Components.Topbar
 | 
			
		||||
  alias CanneryWeb.HomeLive
 | 
			
		||||
 | 
			
		||||
  # Phoenix LiveDashboard is available only in development by default,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,61 +7,6 @@ defmodule CanneryWeb.ViewHelpers do
 | 
			
		||||
 | 
			
		||||
  use Phoenix.Component
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Phoenix.Component for a <time> element that renders the naivedatetime in the
 | 
			
		||||
  user's local timezone with Alpine.js
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  attr :datetime, :any, required: true, doc: "A `DateTime` struct or nil"
 | 
			
		||||
 | 
			
		||||
  def datetime(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <time
 | 
			
		||||
      :if={@datetime}
 | 
			
		||||
      datetime={cast_datetime(@datetime)}
 | 
			
		||||
      x-data={"{
 | 
			
		||||
        datetime:
 | 
			
		||||
          Intl.DateTimeFormat([], {dateStyle: 'short', timeStyle: 'long'})
 | 
			
		||||
            .format(new Date(\"#{cast_datetime(@datetime)}\"))
 | 
			
		||||
      }"}
 | 
			
		||||
      x-text="datetime"
 | 
			
		||||
    >
 | 
			
		||||
      <%= cast_datetime(@datetime) %>
 | 
			
		||||
    </time>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec cast_datetime(NaiveDateTime.t() | nil) :: String.t()
 | 
			
		||||
  defp cast_datetime(%NaiveDateTime{} = datetime) do
 | 
			
		||||
    datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_iso8601(:extended)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp cast_datetime(_datetime), do: ""
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Phoenix.Component for a <date> element that renders the Date in the user's
 | 
			
		||||
  local timezone with Alpine.js
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  attr :date, :any, required: true, doc: "A `Date` struct or nil"
 | 
			
		||||
 | 
			
		||||
  def date(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <time
 | 
			
		||||
      :if={@date}
 | 
			
		||||
      datetime={@date |> Date.to_iso8601(:extended)}
 | 
			
		||||
      x-data={"{
 | 
			
		||||
        date:
 | 
			
		||||
          Intl.DateTimeFormat([], {timeZone: 'Etc/UTC', dateStyle: 'short'})
 | 
			
		||||
            .format(new Date(\"#{@date |> Date.to_iso8601(:extended)}\"))
 | 
			
		||||
      }"}
 | 
			
		||||
      x-text="date"
 | 
			
		||||
    >
 | 
			
		||||
      <%= @date |> Date.to_iso8601(:extended) %>
 | 
			
		||||
    </time>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Displays emoji as text emoji if SHIBAO_MODE is set to true :)
 | 
			
		||||
  """
 | 
			
		||||
@@ -87,23 +32,6 @@ defmodule CanneryWeb.ViewHelpers do
 | 
			
		||||
    "data:image/png;base64," <> img_data
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Creates a downloadable QR Code element
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  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"
 | 
			
		||||
 | 
			
		||||
  def qr_code(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <a href={qr_code_image(@content)} download={@filename <> ".png"}>
 | 
			
		||||
      <img class={@image_class} alt={@filename} src={qr_code_image(@content)} />
 | 
			
		||||
    </a>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Get a random color in `#ffffff` hex format
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user