use core components
This commit is contained in:
		| @@ -1,44 +0,0 @@ | ||||
| defmodule MemexWeb.Components.ContextContent do | ||||
|   @moduledoc """ | ||||
|   Display the content for a context | ||||
|   """ | ||||
|   use MemexWeb, :component | ||||
|   alias Memex.Contexts.Context | ||||
|   alias Phoenix.HTML | ||||
|  | ||||
|   attr :context, Context, required: true | ||||
|  | ||||
|   def context_content(assigns) do | ||||
|     ~H""" | ||||
|     <div | ||||
|       id={"show-context-content-#{@context.id}"} | ||||
|       class="input input-primary h-128 min-h-128 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto" | ||||
|       phx-hook="MaintainAttrs" | ||||
|       phx-update="ignore" | ||||
|       readonly | ||||
|       phx-no-format | ||||
|     ><p class="inline"><%= add_links_to_content(@context.content) %></p></div> | ||||
|     """ | ||||
|   end | ||||
|  | ||||
|   defp add_links_to_content(content) do | ||||
|     Regex.replace( | ||||
|       ~r/\[\[([\p{L}\p{N}\-]+)\]\]/, | ||||
|       content, | ||||
|       fn _whole_match, slug -> | ||||
|         link = | ||||
|           HTML.Link.link( | ||||
|             "[[#{slug}]]", | ||||
|             to: Routes.note_show_path(Endpoint, :show, slug), | ||||
|             class: "link inline", | ||||
|             data: [qa: "context-note-#{slug}"] | ||||
|           ) | ||||
|           |> HTML.Safe.to_iodata() | ||||
|           |> IO.iodata_to_binary() | ||||
|  | ||||
|         "</p>#{link}<p class=\"inline\">" | ||||
|       end | ||||
|     ) | ||||
|     |> HTML.raw() | ||||
|   end | ||||
| end | ||||
							
								
								
									
										204
									
								
								lib/memex_web/components/core_components.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								lib/memex_web/components/core_components.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,204 @@ | ||||
| defmodule MemexWeb.CoreComponents do | ||||
|   @moduledoc """ | ||||
|   Provides core UI components. | ||||
|   """ | ||||
|   use Phoenix.Component | ||||
|   import MemexWeb.{Gettext, ViewHelpers} | ||||
|   alias Memex.{Accounts, Accounts.Invite, Accounts.Invites, Accounts.User} | ||||
|   alias Memex.Contexts.Context | ||||
|   alias Memex.Notes.Note | ||||
|   alias Memex.Pipelines.Steps.Step | ||||
|   alias MemexWeb.{Endpoint, HomeLive} | ||||
|   alias MemexWeb.Router.Helpers, as: Routes | ||||
|   alias Phoenix.HTML | ||||
|   alias Phoenix.LiveView.JS | ||||
|  | ||||
|   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 :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="px-8 py-4 flex flex-col justify-center items-center space-y-4 | ||||
|       bg-primary-900 | ||||
|       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-primary-400 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 :note, Note, required: true | ||||
|  | ||||
|   def note_content(assigns) do | ||||
|     ~H""" | ||||
|     <div | ||||
|       id={"show-note-content-#{@note.id}"} | ||||
|       class="input input-primary h-128 min-h-128 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto" | ||||
|       phx-hook="MaintainAttrs" | ||||
|       phx-update="ignore" | ||||
|       readonly | ||||
|       phx-no-format | ||||
|     ><p class="inline"><%= add_links_to_content(@note.content, "note-link") %></p></div> | ||||
|     """ | ||||
|   end | ||||
|  | ||||
|   attr :context, Context, required: true | ||||
|  | ||||
|   def context_content(assigns) do | ||||
|     ~H""" | ||||
|     <div | ||||
|       id={"show-context-content-#{@context.id}"} | ||||
|       class="input input-primary h-128 min-h-128 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto" | ||||
|       phx-hook="MaintainAttrs" | ||||
|       phx-update="ignore" | ||||
|       readonly | ||||
|       phx-no-format | ||||
|     ><p class="inline"><%= add_links_to_content(@context.content, "context-note") %></p></div> | ||||
|     """ | ||||
|   end | ||||
|  | ||||
|   attr :step, Step, required: true | ||||
|  | ||||
|   def step_content(assigns) do | ||||
|     ~H""" | ||||
|     <div | ||||
|       id={"show-step-content-#{@step.id}"} | ||||
|       class="input input-primary h-32 min-h-32 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto" | ||||
|       phx-hook="MaintainAttrs" | ||||
|       phx-update="ignore" | ||||
|       readonly | ||||
|       phx-no-format | ||||
|     ><p class="inline"><%= add_links_to_content(@step.content, "step-context") %></p></div> | ||||
|     """ | ||||
|   end | ||||
|  | ||||
|   defp add_links_to_content(content, data_qa_prefix) do | ||||
|     Regex.replace( | ||||
|       ~r/\[\[([\p{L}\p{N}\-]+)\]\]/, | ||||
|       content, | ||||
|       fn _whole_match, slug -> | ||||
|         link = | ||||
|           HTML.Link.link( | ||||
|             "[[#{slug}]]", | ||||
|             to: Routes.note_show_path(Endpoint, :show, slug), | ||||
|             class: "link inline", | ||||
|             data: [qa: "#{data_qa_prefix}-#{slug}"] | ||||
|           ) | ||||
|           |> HTML.Safe.to_iodata() | ||||
|           |> IO.iodata_to_binary() | ||||
|  | ||||
|         "</p>#{link}<p class=\"inline\">" | ||||
|       end | ||||
|     ) | ||||
|     |> HTML.raw() | ||||
|   end | ||||
| end | ||||
							
								
								
									
										41
									
								
								lib/memex_web/components/core_components/modal.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								lib/memex_web/components/core_components/modal.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| <.link | ||||
|   id="modal-bg" | ||||
|   patch={@return_to} | ||||
|   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 max-w-3xl max-h-3xl relative w-full | ||||
|       pointer-events-auto overflow-hidden | ||||
|       px-8 py-4 sm:py-8 flex flex-col justify-start items-stretch | ||||
|       bg-primary-800 text-primary-400 border-primary-900 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-stretch"> | ||||
|       <%= render_slot(@inner_block) %> | ||||
|     </div> | ||||
|   </div> | ||||
| </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> | ||||
							
								
								
									
										113
									
								
								lib/memex_web/components/core_components/topbar.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								lib/memex_web/components/core_components/topbar.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| <nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-900 text-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="mx-2 my-1 leading-5 text-xl text-primary-400 hover:underline" | ||||
|       > | ||||
|         <%= gettext("memEx") %> | ||||
|       </.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-primary-400 text-ellipsis"> | ||||
|       <li class="mx-2 my-1"> | ||||
|         <.link | ||||
|           navigate={Routes.note_index_path(Endpoint, :index)} | ||||
|           class="text-primary-400 hover:underline truncate" | ||||
|         > | ||||
|           <%= gettext("notes") %> | ||||
|         </.link> | ||||
|       </li> | ||||
|  | ||||
|       <li class="mx-2 my-1"> | ||||
|         <.link | ||||
|           navigate={Routes.context_index_path(Endpoint, :index)} | ||||
|           class="text-primary-400 hover:underline truncate" | ||||
|         > | ||||
|           <%= gettext("contexts") %> | ||||
|         </.link> | ||||
|       </li> | ||||
|  | ||||
|       <li class="mx-2 my-1"> | ||||
|         <.link | ||||
|           navigate={Routes.pipeline_index_path(Endpoint, :index)} | ||||
|           class="text-primary-400 hover:underline truncate" | ||||
|         > | ||||
|           <%= gettext("pipelines") %> | ||||
|         </.link> | ||||
|       </li> | ||||
|  | ||||
|       <li class="mx-2 my-1 border-left border border-primary-700"></li> | ||||
|  | ||||
|       <%= if @current_user do %> | ||||
|         <li :if={@current_user |> Accounts.is_already_admin?()} class="mx-2 my-1"> | ||||
|           <.link | ||||
|             navigate={Routes.invite_index_path(Endpoint, :index)} | ||||
|             class="text-primary-400 hover:underline" | ||||
|           > | ||||
|             <%= gettext("invites") %> | ||||
|           </.link> | ||||
|         </li> | ||||
|  | ||||
|         <li class="mx-2 my-1"> | ||||
|           <.link | ||||
|             navigate={Routes.user_settings_path(Endpoint, :edit)} | ||||
|             class="text-primary-400 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.role == :admin and function_exported?(Routes, :live_dashboard_path, 2) | ||||
|           } | ||||
|           class="mx-2 my-1" | ||||
|         > | ||||
|           <.link | ||||
|             navigate={Routes.live_dashboard_path(Endpoint, :home)} | ||||
|             class="text-primary-400 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-primary-400 hover:underline truncate" | ||||
|           > | ||||
|             <%= dgettext("actions", "register") %> | ||||
|           </.link> | ||||
|         </li> | ||||
|  | ||||
|         <li class="mx-2 my-1"> | ||||
|           <.link | ||||
|             href={Routes.user_session_path(Endpoint, :new)} | ||||
|             class="text-primary-400 hover:underline truncate" | ||||
|           > | ||||
|             <%= dgettext("actions", "log in") %> | ||||
|           </.link> | ||||
|         </li> | ||||
|       <% end %> | ||||
|     </ul> | ||||
|   </div> | ||||
| </nav> | ||||
							
								
								
									
										37
									
								
								lib/memex_web/components/core_components/user_card.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								lib/memex_web/components/core_components/user_card.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| <div | ||||
|   id={"user-#{@user.id}"} | ||||
|   class="px-8 py-4 flex flex-col justify-center items-center text-center | ||||
|     bg-primary-900 | ||||
|     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 confirmed on%{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,72 +0,0 @@ | ||||
| defmodule MemexWeb.Components.InviteCard do | ||||
|   @moduledoc """ | ||||
|   Display card for an invite | ||||
|   """ | ||||
|  | ||||
|   use MemexWeb, :component | ||||
|   alias Memex.Accounts.{Invite, Invites, User} | ||||
|   alias MemexWeb.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="px-8 py-4 flex flex-col justify-center items-center space-y-4 | ||||
|       bg-primary-900 | ||||
|       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-primary-400 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,44 +0,0 @@ | ||||
| defmodule MemexWeb.Components.NoteContent do | ||||
|   @moduledoc """ | ||||
|   Display the content for a note | ||||
|   """ | ||||
|   use MemexWeb, :component | ||||
|   alias Memex.Notes.Note | ||||
|   alias Phoenix.HTML | ||||
|  | ||||
|   attr :note, Note, required: true | ||||
|  | ||||
|   def note_content(assigns) do | ||||
|     ~H""" | ||||
|     <div | ||||
|       id={"show-note-content-#{@note.id}"} | ||||
|       class="input input-primary h-128 min-h-128 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto" | ||||
|       phx-hook="MaintainAttrs" | ||||
|       phx-update="ignore" | ||||
|       readonly | ||||
|       phx-no-format | ||||
|     ><p class="inline"><%= add_links_to_content(@note.content) %></p></div> | ||||
|     """ | ||||
|   end | ||||
|  | ||||
|   defp add_links_to_content(content) do | ||||
|     Regex.replace( | ||||
|       ~r/\[\[([\p{L}\p{N}\-]+)\]\]/, | ||||
|       content, | ||||
|       fn _whole_match, slug -> | ||||
|         link = | ||||
|           HTML.Link.link( | ||||
|             "[[#{slug}]]", | ||||
|             to: Routes.note_show_path(Endpoint, :show, slug), | ||||
|             class: "link inline", | ||||
|             data: [qa: "note-link-#{slug}"] | ||||
|           ) | ||||
|           |> HTML.Safe.to_iodata() | ||||
|           |> IO.iodata_to_binary() | ||||
|  | ||||
|         "</p>#{link}<p class=\"inline\">" | ||||
|       end | ||||
|     ) | ||||
|     |> HTML.raw() | ||||
|   end | ||||
| end | ||||
| @@ -1,44 +0,0 @@ | ||||
| defmodule MemexWeb.Components.StepContent do | ||||
|   @moduledoc """ | ||||
|   Display the content for a step | ||||
|   """ | ||||
|   use MemexWeb, :component | ||||
|   alias Memex.Pipelines.Steps.Step | ||||
|   alias Phoenix.HTML | ||||
|  | ||||
|   attr :step, Step, required: true | ||||
|  | ||||
|   def step_content(assigns) do | ||||
|     ~H""" | ||||
|     <div | ||||
|       id={"show-step-content-#{@step.id}"} | ||||
|       class="input input-primary h-32 min-h-32 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto" | ||||
|       phx-hook="MaintainAttrs" | ||||
|       phx-update="ignore" | ||||
|       readonly | ||||
|       phx-no-format | ||||
|     ><p class="inline"><%= add_links_to_content(@step.content) %></p></div> | ||||
|     """ | ||||
|   end | ||||
|  | ||||
|   defp add_links_to_content(content) do | ||||
|     Regex.replace( | ||||
|       ~r/\[\[([\p{L}\p{N}\-]+)\]\]/, | ||||
|       content, | ||||
|       fn _whole_match, slug -> | ||||
|         link = | ||||
|           HTML.Link.link( | ||||
|             "[[#{slug}]]", | ||||
|             to: Routes.context_show_path(Endpoint, :show, slug), | ||||
|             class: "link inline", | ||||
|             data: [qa: "step-context-#{slug}"] | ||||
|           ) | ||||
|           |> HTML.Safe.to_iodata() | ||||
|           |> IO.iodata_to_binary() | ||||
|  | ||||
|         "</p>#{link}<p class=\"inline\">" | ||||
|       end | ||||
|     ) | ||||
|     |> HTML.raw() | ||||
|   end | ||||
| end | ||||
| @@ -1,131 +0,0 @@ | ||||
| defmodule MemexWeb.Components.Topbar do | ||||
|   @moduledoc """ | ||||
|   Component that renders a topbar with user functions/links | ||||
|   """ | ||||
|  | ||||
|   use MemexWeb, :component | ||||
|  | ||||
|   alias Memex.Accounts | ||||
|   alias MemexWeb.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-900 text-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="mx-2 my-1 leading-5 text-xl text-primary-400 hover:underline" | ||||
|           > | ||||
|             <%= gettext("memEx") %> | ||||
|           </.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-primary-400 text-ellipsis"> | ||||
|           <li class="mx-2 my-1"> | ||||
|             <.link | ||||
|               navigate={Routes.note_index_path(Endpoint, :index)} | ||||
|               class="text-primary-400 text-primary-400 hover:underline truncate" | ||||
|             > | ||||
|               <%= gettext("notes") %> | ||||
|             </.link> | ||||
|           </li> | ||||
|  | ||||
|           <li class="mx-2 my-1"> | ||||
|             <.link | ||||
|               navigate={Routes.context_index_path(Endpoint, :index)} | ||||
|               class="text-primary-400 text-primary-400 hover:underline truncate" | ||||
|             > | ||||
|               <%= gettext("contexts") %> | ||||
|             </.link> | ||||
|           </li> | ||||
|  | ||||
|           <li class="mx-2 my-1"> | ||||
|             <.link | ||||
|               navigate={Routes.pipeline_index_path(Endpoint, :index)} | ||||
|               class="text-primary-400 text-primary-400 hover:underline truncate" | ||||
|             > | ||||
|               <%= gettext("pipelines") %> | ||||
|             </.link> | ||||
|           </li> | ||||
|  | ||||
|           <li class="mx-2 my-1 border-left border border-primary-700"></li> | ||||
|  | ||||
|           <%= if @current_user do %> | ||||
|             <li :if={@current_user |> Accounts.is_already_admin?()} class="mx-2 my-1"> | ||||
|               <.link | ||||
|                 navigate={Routes.invite_index_path(Endpoint, :index)} | ||||
|                 class="text-primary-400 text-primary-400 hover:underline" | ||||
|               > | ||||
|                 <%= gettext("invites") %> | ||||
|               </.link> | ||||
|             </li> | ||||
|  | ||||
|             <li class="mx-2 my-1"> | ||||
|               <.link | ||||
|                 navigate={Routes.user_settings_path(Endpoint, :edit)} | ||||
|                 class="text-primary-400 text-primary-400 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.role == :admin and function_exported?(Routes, :live_dashboard_path, 2) | ||||
|               } | ||||
|               class="mx-2 my-1" | ||||
|             > | ||||
|               <.link | ||||
|                 navigate={Routes.live_dashboard_path(Endpoint, :home)} | ||||
|                 class="text-primary-400 text-primary-400 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-primary-400 text-primary-400 hover:underline truncate" | ||||
|               > | ||||
|                 <%= dgettext("actions", "register") %> | ||||
|               </.link> | ||||
|             </li> | ||||
|  | ||||
|             <li class="mx-2 my-1"> | ||||
|               <.link | ||||
|                 href={Routes.user_session_path(Endpoint, :new)} | ||||
|                 class="text-primary-400 text-primary-400 hover:underline truncate" | ||||
|               > | ||||
|                 <%= dgettext("actions", "log in") %> | ||||
|               </.link> | ||||
|             </li> | ||||
|           <% end %> | ||||
|         </ul> | ||||
|       </div> | ||||
|     </nav> | ||||
|     """ | ||||
|   end | ||||
| end | ||||
| @@ -1,53 +0,0 @@ | ||||
| defmodule MemexWeb.Components.UserCard do | ||||
|   @moduledoc """ | ||||
|   Display card for a user | ||||
|   """ | ||||
|  | ||||
|   use MemexWeb, :component | ||||
|   alias Memex.Accounts.User | ||||
|  | ||||
|   attr :user, User, required: true | ||||
|   slot(:inner_block, required: true) | ||||
|  | ||||
|   def user_card(assigns) do | ||||
|     ~H""" | ||||
|     <div | ||||
|       id={"user-#{@user.id}"} | ||||
|       class="px-8 py-4 flex flex-col justify-center items-center text-center | ||||
|         bg-primary-900 | ||||
|         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 confirmed on%{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 | ||||
| @@ -5,7 +5,8 @@ | ||||
|  | ||||
|   <.form | ||||
|     :let={f} | ||||
|     for={:search} | ||||
|     for={%{}} | ||||
|     as={:search} | ||||
|     phx-change="search" | ||||
|     phx-submit="search" | ||||
|     class="self-stretch flex flex-col items-stretch" | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| defmodule MemexWeb.ContextLive.Show do | ||||
|   use MemexWeb, :live_view | ||||
|   import MemexWeb.Components.ContextContent | ||||
|   alias Memex.{Accounts.User, Contexts, Contexts.Context} | ||||
|  | ||||
|   @impl true | ||||
|   | ||||
| @@ -4,7 +4,6 @@ defmodule MemexWeb.InviteLive.Index do | ||||
|   """ | ||||
|  | ||||
|   use MemexWeb, :live_view | ||||
|   import MemexWeb.Components.{InviteCard, UserCard} | ||||
|   alias Memex.Accounts | ||||
|   alias Memex.Accounts.{Invite, Invites} | ||||
|   alias MemexWeb.HomeLive | ||||
|   | ||||
| @@ -1,135 +0,0 @@ | ||||
| defmodule MemexWeb.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 | ||||
|       id="modal-bg" | ||||
|       patch={@return_to} | ||||
|       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 max-w-3xl max-h-3xl relative w-full | ||||
|           pointer-events-auto overflow-hidden | ||||
|           px-8 py-4 sm:py-8 flex flex-col justify-start items-stretch | ||||
|           bg-primary-800 text-primary-400 border-primary-900 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-stretch"> | ||||
|           <%= 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 | ||||
| @@ -5,7 +5,8 @@ | ||||
|  | ||||
|   <.form | ||||
|     :let={f} | ||||
|     for={:search} | ||||
|     for={%{}} | ||||
|     as={:search} | ||||
|     phx-change="search" | ||||
|     phx-submit="search" | ||||
|     class="self-stretch flex flex-col items-stretch" | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| defmodule MemexWeb.NoteLive.Show do | ||||
|   use MemexWeb, :live_view | ||||
|   import MemexWeb.Components.NoteContent | ||||
|   alias Memex.{Accounts.User, Notes, Notes.Note} | ||||
|  | ||||
|   @impl true | ||||
|   | ||||
| @@ -5,7 +5,8 @@ | ||||
|  | ||||
|   <.form | ||||
|     :let={f} | ||||
|     for={:search} | ||||
|     for={%{}} | ||||
|     as={:search} | ||||
|     phx-change="search" | ||||
|     phx-submit="search" | ||||
|     class="self-stretch flex flex-col items-stretch" | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| defmodule MemexWeb.PipelineLive.Show do | ||||
|   use MemexWeb, :live_view | ||||
|   import MemexWeb.Components.StepContent | ||||
|   alias Memex.{Accounts.User, Pipelines} | ||||
|   alias Memex.Pipelines.{Pipeline, Steps, Steps.Step} | ||||
|  | ||||
|   | ||||
| @@ -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 MemexWeb.ErrorView do | ||||
|   use MemexWeb, :view | ||||
|   import MemexWeb.Components.Topbar | ||||
|   alias MemexWeb.HomeLive | ||||
|  | ||||
|   def template_not_found(error_path, _assigns) do | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| defmodule MemexWeb.LayoutView do | ||||
|   use MemexWeb, :view | ||||
|   import MemexWeb.Components.Topbar | ||||
|   alias MemexWeb.HomeLive | ||||
|  | ||||
|   # Phoenix LiveDashboard is available only in development by default, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user