rename to memex
This commit is contained in:
		
							
								
								
									
										50
									
								
								lib/memex_web/components/invite_card.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								lib/memex_web/components/invite_card.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| defmodule MemexWeb.Components.InviteCard do | ||||
|   @moduledoc """ | ||||
|   Display card for an invite | ||||
|   """ | ||||
|  | ||||
|   use MemexWeb, :component | ||||
|   alias MemexWeb.Endpoint | ||||
|  | ||||
|   def invite_card(assigns) do | ||||
|     ~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"> | ||||
|           <%= gettext("Uses Left:") %> | ||||
|           <%= @invite.uses_left || "Unlimited" %> | ||||
|         </h2> | ||||
|       <% else %> | ||||
|         <h2 class="title text-md"> | ||||
|           <%= gettext("Invite Disabled") %> | ||||
|         </h2> | ||||
|       <% end %> | ||||
|  | ||||
|       <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" | ||||
|         > | ||||
|           <%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %> | ||||
|         </code> | ||||
|  | ||||
|         <%= if @code_actions do %> | ||||
|           <%= render_slot(@code_actions) %> | ||||
|         <% end %> | ||||
|       </div> | ||||
|  | ||||
|       <%= if @inner_block do %> | ||||
|         <div class="flex space-x-4 justify-center items-center"> | ||||
|           <%= render_slot(@inner_block) %> | ||||
|         </div> | ||||
|       <% end %> | ||||
|     </div> | ||||
|     """ | ||||
|   end | ||||
| end | ||||
							
								
								
									
										87
									
								
								lib/memex_web/components/table_component.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								lib/memex_web/components/table_component.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| defmodule MemexWeb.Components.TableComponent do | ||||
|   @moduledoc """ | ||||
|   Livecomponent that presents a resortable table | ||||
|  | ||||
|   It takes the following required assigns: | ||||
|     - `:columns`: An array of maps containing the following keys | ||||
|       - `:label`: A gettext'd or otherwise user-facing string label for the | ||||
|         column. Can be nil | ||||
|       - `:key`: A string key used for sorting | ||||
|       - `:class`: Extra classes to be applied to the column element, if desired. | ||||
|         Optional | ||||
|       - `:sortable`: If false, will prevent the user from sorting with it. | ||||
|         Optional | ||||
|     - `:values`: An array of maps containing data for each row. Each map is | ||||
|       string-keyed with the associated column key to the following values: | ||||
|       - A single element, like string, integer or Phoenix.LiveView.Rendered | ||||
|         object, like returned from the ~H sigil | ||||
|       - A tuple, containing a custom value used for sorting, and the displayed | ||||
|         content. | ||||
|   """ | ||||
|  | ||||
|   use MemexWeb, :live_component | ||||
|   alias Phoenix.LiveView.Socket | ||||
|  | ||||
|   @impl true | ||||
|   @spec update( | ||||
|           %{ | ||||
|             required(:columns) => | ||||
|               list(%{ | ||||
|                 required(:label) => String.t() | nil, | ||||
|                 required(:key) => String.t() | nil, | ||||
|                 optional(:class) => String.t(), | ||||
|                 optional(:sortable) => false | ||||
|               }), | ||||
|             required(:rows) => | ||||
|               list(%{ | ||||
|                 (key :: String.t()) => any() | {custom_sort_value :: String.t(), value :: any()} | ||||
|               }), | ||||
|             optional(any()) => any() | ||||
|           }, | ||||
|           Socket.t() | ||||
|         ) :: {:ok, Socket.t()} | ||||
|   def update(%{columns: columns, rows: rows} = assigns, socket) do | ||||
|     initial_key = columns |> List.first() |> Map.get(:key) | ||||
|     rows = rows |> Enum.sort_by(fn row -> row |> Map.get(initial_key) end, :asc) | ||||
|  | ||||
|     socket = | ||||
|       socket | ||||
|       |> assign(assigns) | ||||
|       |> assign(columns: columns, rows: rows, last_sort_key: initial_key, sort_mode: :asc) | ||||
|  | ||||
|     {:ok, socket} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event( | ||||
|         "sort_by", | ||||
|         %{"sort-key" => key}, | ||||
|         %{assigns: %{rows: rows, last_sort_key: key, sort_mode: sort_mode}} = socket | ||||
|       ) do | ||||
|     sort_mode = if sort_mode == :asc, do: :desc, else: :asc | ||||
|     rows = rows |> sort_by_custom_sort_value_or_value(key, sort_mode) | ||||
|     {:noreply, socket |> assign(sort_mode: sort_mode, rows: rows)} | ||||
|   end | ||||
|  | ||||
|   def handle_event( | ||||
|         "sort_by", | ||||
|         %{"sort-key" => key}, | ||||
|         %{assigns: %{rows: rows}} = socket | ||||
|       ) do | ||||
|     rows = rows |> sort_by_custom_sort_value_or_value(key, :asc) | ||||
|     {:noreply, socket |> assign(last_sort_key: key, sort_mode: :asc, rows: rows)} | ||||
|   end | ||||
|  | ||||
|   defp sort_by_custom_sort_value_or_value(rows, key, sort_mode) do | ||||
|     rows | ||||
|     |> Enum.sort_by( | ||||
|       fn row -> | ||||
|         case row |> Map.get(key) do | ||||
|           {custom_sort_key, _value} -> custom_sort_key | ||||
|           value -> value | ||||
|         end | ||||
|       end, | ||||
|       sort_mode | ||||
|     ) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										52
									
								
								lib/memex_web/components/table_component.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								lib/memex_web/components/table_component.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| <div class="w-full overflow-x-auto border border-gray-600 rounded-lg shadow-lg bg-black"> | ||||
|   <table class="min-w-full table-auto text-center bg-white"> | ||||
|     <thead class="border-b border-primary-600"> | ||||
|       <tr> | ||||
|         <%= for %{key: key, label: label} = column <- @columns do %> | ||||
|           <%= if column |> Map.get(:sortable, true) do %> | ||||
|             <th class={"p-2 #{column[:class]}"}> | ||||
|               <span | ||||
|                 class="cursor-pointer" | ||||
|                 phx-click="sort_by" | ||||
|                 phx-value-sort-key={key} | ||||
|                 phx-target={@myself} | ||||
|               > | ||||
|                 <span class="underline"><%= label %></span> | ||||
|                 <%= if @last_sort_key == key do %> | ||||
|                   <%= case @sort_mode do %> | ||||
|                     <% :asc -> %> | ||||
|                       <i class="fas fa-sm fa-chevron-down"></i> | ||||
|                     <% :desc -> %> | ||||
|                       <i class="fas fa-sm fa-chevron-up"></i> | ||||
|                   <% end %> | ||||
|                 <% else %> | ||||
|                   <i class="fas fa-sm fa-chevron-up opacity-0"></i> | ||||
|                 <% end %> | ||||
|               </span> | ||||
|             </th> | ||||
|           <% else %> | ||||
|             <th class={"p-2 #{column[:class]}"}> | ||||
|               <%= label %> | ||||
|             </th> | ||||
|           <% end %> | ||||
|         <% end %> | ||||
|       </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|       <%= for values <- @rows do %> | ||||
|         <tr> | ||||
|           <%= for %{key: key} = value <- @columns do %> | ||||
|             <td class={"p-2 #{value[:class]}"}> | ||||
|               <%= case values |> Map.get(key) do %> | ||||
|                 <% {_custom_sort_value, value} -> %> | ||||
|                   <%= value %> | ||||
|                 <% value -> %> | ||||
|                   <%= value %> | ||||
|               <% end %> | ||||
|             </td> | ||||
|           <% end %> | ||||
|         </tr> | ||||
|       <% end %> | ||||
|     </tbody> | ||||
|   </table> | ||||
| </div> | ||||
							
								
								
									
										104
									
								
								lib/memex_web/components/topbar.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								lib/memex_web/components/topbar.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| defmodule MemexWeb.Components.Topbar do | ||||
|   @moduledoc """ | ||||
|   Component that renders a topbar with user functions/links | ||||
|   """ | ||||
|  | ||||
|   use MemexWeb, :component | ||||
|  | ||||
|   alias Memex.Accounts | ||||
|   alias MemexWeb.{Endpoint, 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"> | ||||
|           <%= live_redirect("Memex", | ||||
|             to: Routes.live_path(Endpoint, HomeLive), | ||||
|             class: "mx-2 my-1 leading-5 text-xl text-white hover:underline" | ||||
|           ) %> | ||||
|  | ||||
|           <%= 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 %> | ||||
|             <form phx-change="suggest" phx-submit="search"> | ||||
|               <input | ||||
|                 type="text" | ||||
|                 name="q" | ||||
|                 class="input input-primary" | ||||
|                 placeholder="Search" | ||||
|                 list="results" | ||||
|                 autocomplete="off" | ||||
|               /> | ||||
|               <datalist id="results"> | ||||
|                 <%= for {app, _vsn} <- @results do %> | ||||
|                   <option value={app}> | ||||
|                     "> <%= app %> | ||||
|                   </option> | ||||
|                 <% end %> | ||||
|               </datalist> | ||||
|             </form> | ||||
|             <%= if @current_user.role == :admin do %> | ||||
|               <li class="mx-2 my-1"> | ||||
|                 <%= live_redirect(gettext("Invites"), | ||||
|                   to: Routes.invite_index_path(Endpoint, :index), | ||||
|                   class: "text-white text-white hover:underline" | ||||
|                 ) %> | ||||
|               </li> | ||||
|             <% end %> | ||||
|             <li class="mx-2 my-1"> | ||||
|               <%= live_redirect(@current_user.email, | ||||
|                 to: Routes.user_settings_path(Endpoint, :edit), | ||||
|                 class: "text-white text-white hover:underline truncate" | ||||
|               ) %> | ||||
|             </li> | ||||
|             <li class="mx-2 my-1"> | ||||
|               <%= link to: Routes.user_session_path(Endpoint, :delete), | ||||
|                    method: :delete, | ||||
|                    data: [confirm: dgettext("prompts", "Are you sure you want to log out?")] do %> | ||||
|                 <i class="fas fa-sign-out-alt"></i> | ||||
|               <% end %> | ||||
|             </li> | ||||
|             <%= if @current_user.role == :admin and function_exported?(Routes, :live_dashboard_path, 2) do %> | ||||
|               <li class="mx-2 my-1"> | ||||
|                 <%= live_redirect to: Routes.live_dashboard_path(Endpoint, :home), | ||||
|                   class: "text-white text-white hover:underline" do %> | ||||
|                   <i class="fas fa-gauge"></i> | ||||
|                 <% end %> | ||||
|               </li> | ||||
|             <% end %> | ||||
|           <% else %> | ||||
|             <%= if Accounts.allow_registration?() do %> | ||||
|               <li class="mx-2 my-1"> | ||||
|                 <%= live_redirect(dgettext("actions", "Register"), | ||||
|                   to: Routes.user_registration_path(Endpoint, :new), | ||||
|                   class: "text-white text-white hover:underline truncate" | ||||
|                 ) %> | ||||
|               </li> | ||||
|             <% end %> | ||||
|             <li class="mx-2 my-1"> | ||||
|               <%= live_redirect(dgettext("actions", "Log in"), | ||||
|                 to: Routes.user_session_path(Endpoint, :new), | ||||
|                 class: "text-white text-white hover:underline truncate" | ||||
|               ) %> | ||||
|             </li> | ||||
|           <% end %> | ||||
|         </ul> | ||||
|       </div> | ||||
|     </nav> | ||||
|     """ | ||||
|   end | ||||
| end | ||||
							
								
								
									
										43
									
								
								lib/memex_web/components/user_card.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								lib/memex_web/components/user_card.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| defmodule MemexWeb.Components.UserCard do | ||||
|   @moduledoc """ | ||||
|   Display card for a user | ||||
|   """ | ||||
|  | ||||
|   use MemexWeb, :component | ||||
|  | ||||
|   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 |> is_nil() do %> | ||||
|             Email unconfirmed | ||||
|           <% else %> | ||||
|             User was confirmed at <%= @user.confirmed_at |> display_datetime() %> | ||||
|           <% end %> | ||||
|         </p> | ||||
|  | ||||
|         <p> | ||||
|           <%= gettext("User registered on") %> | ||||
|           <%= @user.inserted_at |> display_datetime() %> | ||||
|         </p> | ||||
|       </h3> | ||||
|  | ||||
|       <%= if @inner_block do %> | ||||
|         <div class="px-4 py-2 flex space-x-4 justify-center items-center"> | ||||
|           <%= render_slot(@inner_block) %> | ||||
|         </div> | ||||
|       <% end %> | ||||
|     </div> | ||||
|     """ | ||||
|   end | ||||
| end | ||||
							
								
								
									
										23
									
								
								lib/memex_web/controllers/email_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								lib/memex_web/controllers/email_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| defmodule MemexWeb.EmailController do | ||||
|   @moduledoc """ | ||||
|   A dev controller used to develop on emails | ||||
|   """ | ||||
|  | ||||
|   use MemexWeb, :controller | ||||
|   alias Memex.Accounts.User | ||||
|  | ||||
|   plug :put_layout, {MemexWeb.LayoutView, :email} | ||||
|  | ||||
|   @sample_assigns %{ | ||||
|     email: %{subject: "Example subject"}, | ||||
|     url: "https://memex.bubbletea.dev/sample_url", | ||||
|     user: %User{email: "sample@email.com"} | ||||
|   } | ||||
|  | ||||
|   @doc """ | ||||
|   Debug route used to preview emails | ||||
|   """ | ||||
|   def preview(conn, %{"id" => template}) do | ||||
|     render(conn, "#{template |> to_string()}.html", @sample_assigns) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										11
									
								
								lib/memex_web/controllers/home_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								lib/memex_web/controllers/home_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| defmodule MemexWeb.HomeController do | ||||
|   @moduledoc """ | ||||
|   Controller for home page | ||||
|   """ | ||||
|  | ||||
|   use MemexWeb, :controller | ||||
|  | ||||
|   def index(conn, _params) do | ||||
|     render(conn, "index.html") | ||||
|   end | ||||
| end | ||||
							
								
								
									
										191
									
								
								lib/memex_web/controllers/user_auth.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								lib/memex_web/controllers/user_auth.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,191 @@ | ||||
| defmodule MemexWeb.UserAuth do | ||||
|   @moduledoc """ | ||||
|   Functions for user session and authentication | ||||
|   """ | ||||
|  | ||||
|   import Plug.Conn | ||||
|   import Phoenix.Controller | ||||
|   import MemexWeb.Gettext | ||||
|   alias Memex.{Accounts, Accounts.User} | ||||
|   alias MemexWeb.HomeLive | ||||
|   alias MemexWeb.Router.Helpers, as: Routes | ||||
|  | ||||
|   # Make the remember me cookie valid for 60 days. | ||||
|   # If you want bump or reduce this value, also change | ||||
|   # the token expiry itself in UserToken. | ||||
|   @max_age 60 * 60 * 24 * 60 | ||||
|   @remember_me_cookie "_memex_web_user_remember_me" | ||||
|   @remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"] | ||||
|  | ||||
|   @doc """ | ||||
|   Logs the user in. | ||||
|  | ||||
|   It renews the session ID and clears the whole session | ||||
|   to avoid fixation attacks. See the renew_session | ||||
|   function to customize this behaviour. | ||||
|  | ||||
|   It also sets a `:live_socket_id` key in the session, | ||||
|   so LiveView sessions are identified and automatically | ||||
|   disconnected on log out. The line can be safely removed | ||||
|   if you are not using LiveView. | ||||
|   """ | ||||
|   def log_in_user(conn, user, params \\ %{}) | ||||
|  | ||||
|   def log_in_user(conn, %User{confirmed_at: nil}, _params) do | ||||
|     conn | ||||
|     |> fetch_flash() | ||||
|     |> put_flash( | ||||
|       :error, | ||||
|       dgettext("errors", "You must confirm your account and log in to access this page.") | ||||
|     ) | ||||
|     |> maybe_store_return_to() | ||||
|     |> redirect(to: Routes.user_session_path(conn, :new)) | ||||
|     |> halt() | ||||
|   end | ||||
|  | ||||
|   def log_in_user(conn, user, params) do | ||||
|     token = Accounts.generate_user_session_token(user) | ||||
|     user_return_to = get_session(conn, :user_return_to) | ||||
|  | ||||
|     conn | ||||
|     |> renew_session() | ||||
|     |> put_session(:user_token, token) | ||||
|     |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}") | ||||
|     |> maybe_write_remember_me_cookie(token, params) | ||||
|     |> redirect(to: user_return_to || signed_in_path(conn)) | ||||
|   end | ||||
|  | ||||
|   @spec maybe_write_remember_me_cookie( | ||||
|           Plug.Conn.t(), | ||||
|           String.t() | any(), | ||||
|           %{required(String.t()) => String.t()} | any() | ||||
|         ) :: Plug.Conn.t() | ||||
|   defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do | ||||
|     put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options) | ||||
|   end | ||||
|  | ||||
|   defp maybe_write_remember_me_cookie(conn, _token, _params) do | ||||
|     conn | ||||
|   end | ||||
|  | ||||
|   # This function renews the session ID and erases the whole | ||||
|   # session to avoid fixation attacks. If there is any data | ||||
|   # in the session you may want to preserve after log in/log out, | ||||
|   # you must explicitly fetch the session data before clearing | ||||
|   # and then immediately set it after clearing, for example: | ||||
|   # | ||||
|   #     defp renew_session(conn) do | ||||
|   #       preferred_locale = get_session(conn, :preferred_locale) | ||||
|   # | ||||
|   #       conn | ||||
|   #       |> configure_session(renew: true) | ||||
|   #       |> clear_session() | ||||
|   #       |> put_session(:preferred_locale, preferred_locale) | ||||
|   #     end | ||||
|   # | ||||
|   defp renew_session(conn) do | ||||
|     conn | ||||
|     |> configure_session(renew: true) | ||||
|     |> clear_session() | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Logs the user out. | ||||
|  | ||||
|   It clears all session data for safety. See renew_session. | ||||
|   """ | ||||
|   def log_out_user(conn) do | ||||
|     user_token = get_session(conn, :user_token) | ||||
|     user_token && Accounts.delete_session_token(user_token) | ||||
|  | ||||
|     if live_socket_id = get_session(conn, :live_socket_id) do | ||||
|       MemexWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{}) | ||||
|     end | ||||
|  | ||||
|     conn | ||||
|     |> renew_session() | ||||
|     |> delete_resp_cookie(@remember_me_cookie) | ||||
|     |> redirect(to: "/") | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Authenticates the user by looking into the session | ||||
|   and remember me token. | ||||
|   """ | ||||
|   def fetch_current_user(conn, _opts) do | ||||
|     {user_token, conn} = ensure_user_token(conn) | ||||
|     user = user_token && Accounts.get_user_by_session_token(user_token) | ||||
|     assign(conn, :current_user, user) | ||||
|   end | ||||
|  | ||||
|   defp ensure_user_token(conn) do | ||||
|     if user_token = get_session(conn, :user_token) do | ||||
|       {user_token, conn} | ||||
|     else | ||||
|       conn = fetch_cookies(conn, signed: [@remember_me_cookie]) | ||||
|  | ||||
|       if user_token = conn.cookies[@remember_me_cookie] do | ||||
|         {user_token, put_session(conn, :user_token, user_token)} | ||||
|       else | ||||
|         {nil, conn} | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Used for routes that require the user to not be authenticated. | ||||
|   """ | ||||
|   def redirect_if_user_is_authenticated(conn, _opts) do | ||||
|     if conn.assigns[:current_user] do | ||||
|       conn | ||||
|       |> redirect(to: signed_in_path(conn)) | ||||
|       |> halt() | ||||
|     else | ||||
|       conn | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Used for routes that require the user to be authenticated. | ||||
|  | ||||
|   If you want to enforce the user email is confirmed before | ||||
|   they use the application at all, here would be a good place. | ||||
|   """ | ||||
|   def require_authenticated_user(conn, _opts) do | ||||
|     if conn.assigns[:current_user] do | ||||
|       conn | ||||
|     else | ||||
|       conn | ||||
|       |> put_flash( | ||||
|         :error, | ||||
|         dgettext("errors", "You must confirm your account and log in to access this page.") | ||||
|       ) | ||||
|       |> maybe_store_return_to() | ||||
|       |> redirect(to: Routes.user_session_path(conn, :new)) | ||||
|       |> halt() | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Used for routes that require the user to be an admin. | ||||
|   """ | ||||
|   def require_role(conn, role: role_atom) do | ||||
|     if conn.assigns[:current_user] && conn.assigns.current_user.role == role_atom do | ||||
|       conn | ||||
|     else | ||||
|       conn | ||||
|       |> put_flash(:error, dgettext("errors", "You are not authorized to view this page.")) | ||||
|       |> maybe_store_return_to() | ||||
|       |> redirect(to: Routes.live_path(conn, HomeLive)) | ||||
|       |> halt() | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp maybe_store_return_to(%{method: "GET"} = conn) do | ||||
|     put_session(conn, :user_return_to, current_path(conn)) | ||||
|   end | ||||
|  | ||||
|   defp maybe_store_return_to(conn), do: conn | ||||
|  | ||||
|   defp signed_in_path(_conn), do: "/" | ||||
| end | ||||
							
								
								
									
										60
									
								
								lib/memex_web/controllers/user_confirmation_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								lib/memex_web/controllers/user_confirmation_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| defmodule MemexWeb.UserConfirmationController do | ||||
|   use MemexWeb, :controller | ||||
|  | ||||
|   import MemexWeb.Gettext | ||||
|   alias Memex.Accounts | ||||
|  | ||||
|   def new(conn, _params) do | ||||
|     render(conn, "new.html", page_title: gettext("Confirm your account")) | ||||
|   end | ||||
|  | ||||
|   def create(conn, %{"user" => %{"email" => email}}) do | ||||
|     if user = Accounts.get_user_by_email(email) do | ||||
|       Accounts.deliver_user_confirmation_instructions( | ||||
|         user, | ||||
|         &Routes.user_confirmation_url(conn, :confirm, &1) | ||||
|       ) | ||||
|     end | ||||
|  | ||||
|     # Regardless of the outcome, show an impartial success/error message. | ||||
|     conn | ||||
|     |> put_flash( | ||||
|       :info, | ||||
|       dgettext( | ||||
|         "prompts", | ||||
|         "If your email is in our system and it has not been confirmed yet, " <> | ||||
|           "you will receive an email with instructions shortly." | ||||
|       ) | ||||
|     ) | ||||
|     |> redirect(to: "/") | ||||
|   end | ||||
|  | ||||
|   # Do not log in the user after confirmation to avoid a | ||||
|   # leaked token giving the user access to the account. | ||||
|   def confirm(conn, %{"token" => token}) do | ||||
|     case Accounts.confirm_user(token) do | ||||
|       {:ok, %{email: email}} -> | ||||
|         conn | ||||
|         |> put_flash(:info, dgettext("prompts", "%{email} confirmed successfully.", email: email)) | ||||
|         |> redirect(to: "/") | ||||
|  | ||||
|       :error -> | ||||
|         # If there is a current user and the account was already confirmed, | ||||
|         # then odds are that the confirmation link was already visited, either | ||||
|         # by some automation or by the user themselves, so we redirect without | ||||
|         # a warning message. | ||||
|         case conn.assigns do | ||||
|           %{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) -> | ||||
|             redirect(conn, to: "/") | ||||
|  | ||||
|           %{} -> | ||||
|             conn | ||||
|             |> put_flash( | ||||
|               :error, | ||||
|               dgettext("errors", "User confirmation link is invalid or it has expired.") | ||||
|             ) | ||||
|             |> redirect(to: "/") | ||||
|         end | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										81
									
								
								lib/memex_web/controllers/user_registration_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								lib/memex_web/controllers/user_registration_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| defmodule MemexWeb.UserRegistrationController do | ||||
|   use MemexWeb, :controller | ||||
|   import MemexWeb.Gettext | ||||
|   alias Memex.{Accounts, Invites} | ||||
|   alias Memex.Accounts.User | ||||
|   alias MemexWeb.{Endpoint, HomeLive} | ||||
|  | ||||
|   def new(conn, %{"invite" => invite_token}) do | ||||
|     invite = Invites.get_invite_by_token(invite_token) | ||||
|  | ||||
|     if invite do | ||||
|       conn |> render_new(invite) | ||||
|     else | ||||
|       conn | ||||
|       |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired")) | ||||
|       |> redirect(to: Routes.live_path(Endpoint, HomeLive)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def new(conn, _params) do | ||||
|     if Accounts.allow_registration?() do | ||||
|       conn |> render_new() | ||||
|     else | ||||
|       conn | ||||
|       |> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled")) | ||||
|       |> redirect(to: Routes.live_path(Endpoint, HomeLive)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   # renders new user registration page | ||||
|   defp render_new(conn, invite \\ nil) do | ||||
|     render(conn, "new.html", | ||||
|       changeset: Accounts.change_user_registration(%User{}), | ||||
|       invite: invite, | ||||
|       page_title: gettext("Register") | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def create(conn, %{"user" => %{"invite_token" => invite_token}} = attrs) do | ||||
|     invite = Invites.get_invite_by_token(invite_token) | ||||
|  | ||||
|     if invite do | ||||
|       conn |> create_user(attrs, invite) | ||||
|     else | ||||
|       conn | ||||
|       |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired")) | ||||
|       |> redirect(to: Routes.live_path(Endpoint, HomeLive)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def create(conn, attrs) do | ||||
|     if Accounts.allow_registration?() do | ||||
|       conn |> create_user(attrs) | ||||
|     else | ||||
|       conn | ||||
|       |> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled")) | ||||
|       |> redirect(to: Routes.live_path(Endpoint, HomeLive)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp create_user(conn, %{"user" => user_params}, invite \\ nil) do | ||||
|     case Accounts.register_user(user_params) do | ||||
|       {:ok, user} -> | ||||
|         unless invite |> is_nil() do | ||||
|           invite |> Invites.use_invite!() | ||||
|         end | ||||
|  | ||||
|         Accounts.deliver_user_confirmation_instructions( | ||||
|           user, | ||||
|           &Routes.user_confirmation_url(conn, :confirm, &1) | ||||
|         ) | ||||
|  | ||||
|         conn | ||||
|         |> put_flash(:info, dgettext("prompts", "Please check your email to verify your account")) | ||||
|         |> redirect(to: Routes.user_session_path(Endpoint, :new)) | ||||
|  | ||||
|       {:error, %Ecto.Changeset{} = changeset} -> | ||||
|         conn |> render("new.html", changeset: changeset, invite: invite) | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										69
									
								
								lib/memex_web/controllers/user_reset_password_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								lib/memex_web/controllers/user_reset_password_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| defmodule MemexWeb.UserResetPasswordController do | ||||
|   use MemexWeb, :controller | ||||
|  | ||||
|   alias Memex.Accounts | ||||
|  | ||||
|   plug :get_user_by_reset_password_token when action in [:edit, :update] | ||||
|  | ||||
|   def new(conn, _params) do | ||||
|     render(conn, "new.html", page_title: gettext("Forgot your password?")) | ||||
|   end | ||||
|  | ||||
|   def create(conn, %{"user" => %{"email" => email}}) do | ||||
|     if user = Accounts.get_user_by_email(email) do | ||||
|       Accounts.deliver_user_reset_password_instructions( | ||||
|         user, | ||||
|         &Routes.user_reset_password_url(conn, :edit, &1) | ||||
|       ) | ||||
|     end | ||||
|  | ||||
|     # Regardless of the outcome, show an impartial success/error message. | ||||
|     conn | ||||
|     |> put_flash( | ||||
|       :info, | ||||
|       dgettext( | ||||
|         "prompts", | ||||
|         "If your email is in our system, you will receive instructions to " <> | ||||
|           "reset your password shortly." | ||||
|       ) | ||||
|     ) | ||||
|     |> redirect(to: "/") | ||||
|   end | ||||
|  | ||||
|   def edit(conn, _params) do | ||||
|     render(conn, "edit.html", | ||||
|       changeset: Accounts.change_user_password(conn.assigns.user), | ||||
|       page_title: gettext("Reset your password") | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   # Do not log in the user after reset password to avoid a | ||||
|   # leaked token giving the user access to the account. | ||||
|   def update(conn, %{"user" => user_params}) do | ||||
|     case Accounts.reset_user_password(conn.assigns.user, user_params) do | ||||
|       {:ok, _} -> | ||||
|         conn | ||||
|         |> put_flash(:info, dgettext("prompts", "Password reset successfully.")) | ||||
|         |> redirect(to: Routes.user_session_path(conn, :new)) | ||||
|  | ||||
|       {:error, changeset} -> | ||||
|         render(conn, "edit.html", changeset: changeset) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp get_user_by_reset_password_token(conn, _opts) do | ||||
|     %{"token" => token} = conn.params | ||||
|  | ||||
|     if user = Accounts.get_user_by_reset_password_token(token) do | ||||
|       conn |> assign(:user, user) |> assign(:token, token) | ||||
|     else | ||||
|       conn | ||||
|       |> put_flash( | ||||
|         :error, | ||||
|         dgettext("errors", "Reset password link is invalid or it has expired.") | ||||
|       ) | ||||
|       |> redirect(to: "/") | ||||
|       |> halt() | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										26
									
								
								lib/memex_web/controllers/user_session_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								lib/memex_web/controllers/user_session_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| defmodule MemexWeb.UserSessionController do | ||||
|   use MemexWeb, :controller | ||||
|  | ||||
|   alias Memex.Accounts | ||||
|   alias MemexWeb.UserAuth | ||||
|  | ||||
|   def new(conn, _params) do | ||||
|     render(conn, "new.html", error_message: nil, page_title: gettext("Log in")) | ||||
|   end | ||||
|  | ||||
|   def create(conn, %{"user" => user_params}) do | ||||
|     %{"email" => email, "password" => password} = user_params | ||||
|  | ||||
|     if user = Accounts.get_user_by_email_and_password(email, password) do | ||||
|       UserAuth.log_in_user(conn, user, user_params) | ||||
|     else | ||||
|       render(conn, "new.html", error_message: dgettext("errors", "Invalid email or password")) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def delete(conn, _params) do | ||||
|     conn | ||||
|     |> put_flash(:info, dgettext("prompts", "Logged out successfully.")) | ||||
|     |> UserAuth.log_out_user() | ||||
|   end | ||||
| end | ||||
							
								
								
									
										110
									
								
								lib/memex_web/controllers/user_settings_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								lib/memex_web/controllers/user_settings_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| defmodule MemexWeb.UserSettingsController do | ||||
|   use MemexWeb, :controller | ||||
|   import MemexWeb.Gettext | ||||
|   alias Memex.Accounts | ||||
|   alias MemexWeb.{HomeLive, UserAuth} | ||||
|  | ||||
|   plug :assign_email_and_password_changesets | ||||
|  | ||||
|   def edit(conn, _params) do | ||||
|     render(conn, "edit.html", page_title: gettext("Settings")) | ||||
|   end | ||||
|  | ||||
|   def update(%{assigns: %{current_user: user}} = conn, %{ | ||||
|         "action" => "update_email", | ||||
|         "current_password" => password, | ||||
|         "user" => user_params | ||||
|       }) do | ||||
|     case Accounts.apply_user_email(user, password, user_params) do | ||||
|       {:ok, applied_user} -> | ||||
|         Accounts.deliver_update_email_instructions( | ||||
|           applied_user, | ||||
|           user.email, | ||||
|           &Routes.user_settings_url(conn, :confirm_email, &1) | ||||
|         ) | ||||
|  | ||||
|         conn | ||||
|         |> put_flash( | ||||
|           :info, | ||||
|           dgettext( | ||||
|             "prompts", | ||||
|             "A link to confirm your email change has been sent to the new address." | ||||
|           ) | ||||
|         ) | ||||
|         |> redirect(to: Routes.user_settings_path(conn, :edit)) | ||||
|  | ||||
|       {:error, changeset} -> | ||||
|         conn |> render("edit.html", email_changeset: changeset) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def update(%{assigns: %{current_user: user}} = conn, %{ | ||||
|         "action" => "update_password", | ||||
|         "current_password" => password, | ||||
|         "user" => user_params | ||||
|       }) do | ||||
|     case Accounts.update_user_password(user, password, user_params) do | ||||
|       {:ok, user} -> | ||||
|         conn | ||||
|         |> put_flash(:info, dgettext("prompts", "Password updated successfully.")) | ||||
|         |> put_session(:user_return_to, Routes.user_settings_path(conn, :edit)) | ||||
|         |> UserAuth.log_in_user(user) | ||||
|  | ||||
|       {:error, changeset} -> | ||||
|         conn |> render("edit.html", password_changeset: changeset) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def update( | ||||
|         %{assigns: %{current_user: user}} = conn, | ||||
|         %{"action" => "update_locale", "user" => %{"locale" => locale}} | ||||
|       ) do | ||||
|     case Accounts.update_user_locale(user, locale) do | ||||
|       {:ok, _user} -> | ||||
|         conn | ||||
|         |> put_flash(:info, dgettext("prompts", "Language updated successfully.")) | ||||
|         |> redirect(to: Routes.user_settings_path(conn, :edit)) | ||||
|  | ||||
|       {:error, changeset} -> | ||||
|         conn |> render("edit.html", locale_changeset: changeset) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def confirm_email(%{assigns: %{current_user: user}} = conn, %{"token" => token}) do | ||||
|     case Accounts.update_user_email(user, token) do | ||||
|       :ok -> | ||||
|         conn | ||||
|         |> put_flash(:info, dgettext("prompts", "Email changed successfully.")) | ||||
|         |> redirect(to: Routes.user_settings_path(conn, :edit)) | ||||
|  | ||||
|       :error -> | ||||
|         conn | ||||
|         |> put_flash( | ||||
|           :error, | ||||
|           dgettext("errors", "Email change link is invalid or it has expired.") | ||||
|         ) | ||||
|         |> redirect(to: Routes.user_settings_path(conn, :edit)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def delete(%{assigns: %{current_user: current_user}} = conn, %{"id" => user_id}) do | ||||
|     if user_id == current_user.id do | ||||
|       current_user |> Accounts.delete_user!(current_user) | ||||
|  | ||||
|       conn | ||||
|       |> put_flash(:error, dgettext("prompts", "Your account has been deleted")) | ||||
|       |> redirect(to: Routes.live_path(conn, HomeLive)) | ||||
|     else | ||||
|       conn | ||||
|       |> put_flash(:error, dgettext("errors", "Unable to delete user")) | ||||
|       |> redirect(to: Routes.user_settings_path(conn, :edit)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp assign_email_and_password_changesets(%{assigns: %{current_user: user}} = conn, _opts) do | ||||
|     conn | ||||
|     |> assign(:email_changeset, Accounts.change_user_email(user)) | ||||
|     |> assign(:password_changeset, Accounts.change_user_password(user)) | ||||
|     |> assign(:locale_changeset, Accounts.change_user_locale(user)) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										50
									
								
								lib/memex_web/endpoint.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								lib/memex_web/endpoint.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| defmodule MemexWeb.Endpoint do | ||||
|   use Phoenix.Endpoint, otp_app: :memex | ||||
|  | ||||
|   # The session will be stored in the cookie and signed, | ||||
|   # this means its contents can be read but not tampered with. | ||||
|   # Set :encryption_salt if you would also like to encrypt it. | ||||
|   @session_options [ | ||||
|     store: :cookie, | ||||
|     key: "_memex_key", | ||||
|     signing_salt: "fxAnJltS" | ||||
|   ] | ||||
|  | ||||
|   socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] | ||||
|  | ||||
|   # Serve at "/" the static files from "priv/static" directory. | ||||
|   # | ||||
|   # You should set gzip to true if you are running phx.digest | ||||
|   # when deploying your static files in production. | ||||
|   plug Plug.Static, | ||||
|     at: "/", | ||||
|     from: :memex, | ||||
|     gzip: false, | ||||
|     only: ~w(css fonts images js favicon.ico robots.txt) | ||||
|  | ||||
|   # Code reloading can be explicitly enabled under the | ||||
|   # :code_reloader configuration of your endpoint. | ||||
|   if code_reloading? do | ||||
|     socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket | ||||
|     plug Phoenix.LiveReloader | ||||
|     plug Phoenix.CodeReloader | ||||
|     plug Phoenix.Ecto.CheckRepoStatus, otp_app: :memex | ||||
|   end | ||||
|  | ||||
|   plug Phoenix.LiveDashboard.RequestLogger, | ||||
|     param_key: "request_logger", | ||||
|     cookie_key: "request_logger" | ||||
|  | ||||
|   plug Plug.RequestId | ||||
|   plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] | ||||
|  | ||||
|   plug Plug.Parsers, | ||||
|     parsers: [:urlencoded, :multipart, :json], | ||||
|     pass: ["*/*"], | ||||
|     json_decoder: Phoenix.json_library() | ||||
|  | ||||
|   plug Plug.MethodOverride | ||||
|   plug Plug.Head | ||||
|   plug Plug.Session, @session_options | ||||
|   plug MemexWeb.Router | ||||
| end | ||||
							
								
								
									
										24
									
								
								lib/memex_web/gettext.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								lib/memex_web/gettext.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| defmodule MemexWeb.Gettext do | ||||
|   @moduledoc """ | ||||
|   A module providing Internationalization with a gettext-based API. | ||||
|  | ||||
|   By using [Gettext](https://hexdocs.pm/gettext), | ||||
|   your module gains a set of macros for translations, for example: | ||||
|  | ||||
|       import MemexWeb.Gettext | ||||
|  | ||||
|       # Simple translation | ||||
|       gettext("Here is the string to translate") | ||||
|  | ||||
|       # Plural translation | ||||
|       ngettext("Here is the string to translate", | ||||
|                "Here are the strings to translate", | ||||
|                3) | ||||
|  | ||||
|       # Domain-based translation | ||||
|       dgettext("errors", "Here is the error message to translate") | ||||
|  | ||||
|   See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. | ||||
|   """ | ||||
|   use Gettext, otp_app: :memex | ||||
| end | ||||
							
								
								
									
										43
									
								
								lib/memex_web/live/home_live.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								lib/memex_web/live/home_live.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| defmodule MemexWeb.HomeLive do | ||||
|   @moduledoc """ | ||||
|   Liveview for the main home page | ||||
|   """ | ||||
|  | ||||
|   use MemexWeb, :live_view | ||||
|  | ||||
|   @impl true | ||||
|   def mount(_params, _session, socket) do | ||||
|     {:ok, socket |> assign(page_title: gettext("Home"), query: "", results: %{})} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event("suggest", %{"q" => query}, socket) do | ||||
|     {:noreply, socket |> assign(results: search(query), query: query)} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event("search", %{"q" => query}, socket) do | ||||
|     case search(query) do | ||||
|       %{^query => vsn} -> | ||||
|         {:noreply, socket |> redirect(external: "https://hexdocs.pm/#{query}/#{vsn}")} | ||||
|  | ||||
|       _ -> | ||||
|         {:noreply, | ||||
|          socket | ||||
|          |> put_flash(:error, "No dependencies found matching \"#{query}\"") | ||||
|          |> assign(results: %{}, query: query)} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp search(query) do | ||||
|     if not MemexWeb.Endpoint.config(:code_reloader) do | ||||
|       raise "action disabled when not in development" | ||||
|     end | ||||
|  | ||||
|     for {app, desc, vsn} <- Application.started_applications(), | ||||
|         app = to_string(app), | ||||
|         String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"), | ||||
|         into: %{}, | ||||
|         do: {app, vsn} | ||||
|   end | ||||
| end | ||||
							
								
								
									
										9
									
								
								lib/memex_web/live/home_live.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib/memex_web/live/home_live.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| <div class="flex flex-col justify-center items-center text-center space-y-4"> | ||||
|   <h1 class="title text-primary-500 text-2xl"> | ||||
|     <%= gettext("memex") %> | ||||
|   </h1> | ||||
|  | ||||
|   <p class="title  text-primary-500 text-lg"> | ||||
|     <%= gettext("filling this out later") %> | ||||
|   </p> | ||||
| </div> | ||||
							
								
								
									
										19
									
								
								lib/memex_web/live/init_assigns.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								lib/memex_web/live/init_assigns.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| defmodule MemexWeb.InitAssigns do | ||||
|   @moduledoc """ | ||||
|   Ensures common `assigns` are applied to all LiveViews attaching this hook. | ||||
|   """ | ||||
|   import Phoenix.LiveView | ||||
|   alias Memex.Accounts | ||||
|  | ||||
|   def on_mount(:default, _params, %{"locale" => locale, "user_token" => user_token}, socket) do | ||||
|     Gettext.put_locale(locale) | ||||
|  | ||||
|     socket = | ||||
|       socket | ||||
|       |> assign_new(:current_user, fn -> Accounts.get_user_by_session_token(user_token) end) | ||||
|  | ||||
|     {:cont, socket} | ||||
|   end | ||||
|  | ||||
|   def on_mount(:default, _params, _session, socket), do: {:cont, socket} | ||||
| end | ||||
							
								
								
									
										72
									
								
								lib/memex_web/live/invite_live/form_component.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								lib/memex_web/live/invite_live/form_component.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| defmodule MemexWeb.InviteLive.FormComponent do | ||||
|   @moduledoc """ | ||||
|   Livecomponent that can update or create an Memex.Invites.Invite | ||||
|   """ | ||||
|  | ||||
|   use MemexWeb, :live_component | ||||
|   alias Ecto.Changeset | ||||
|   alias Memex.{Accounts.User, Invites, Invites.Invite} | ||||
|   alias Phoenix.LiveView.Socket | ||||
|  | ||||
|   @impl true | ||||
|   @spec update( | ||||
|           %{:invite => Invite.t(), :current_user => User.t(), optional(any) => any}, | ||||
|           Socket.t() | ||||
|         ) :: {:ok, Socket.t()} | ||||
|   def update(%{invite: invite} = assigns, socket) do | ||||
|     {:ok, socket |> assign(assigns) |> assign(:changeset, Invites.change_invite(invite))} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event( | ||||
|         "validate", | ||||
|         %{"invite" => invite_params}, | ||||
|         %{assigns: %{invite: invite}} = socket | ||||
|       ) do | ||||
|     {:noreply, socket |> assign(:changeset, invite |> Invites.change_invite(invite_params))} | ||||
|   end | ||||
|  | ||||
|   def handle_event("save", %{"invite" => invite_params}, %{assigns: %{action: action}} = socket) do | ||||
|     save_invite(socket, action, invite_params) | ||||
|   end | ||||
|  | ||||
|   defp save_invite( | ||||
|          %{assigns: %{current_user: current_user, invite: invite, return_to: return_to}} = socket, | ||||
|          :edit, | ||||
|          invite_params | ||||
|        ) do | ||||
|     socket = | ||||
|       case invite |> Invites.update_invite(invite_params, current_user) do | ||||
|         {:ok, %{name: invite_name}} -> | ||||
|           prompt = | ||||
|             dgettext("prompts", "%{invite_name} updated successfully", invite_name: invite_name) | ||||
|  | ||||
|           socket |> put_flash(:info, prompt) |> push_redirect(to: return_to) | ||||
|  | ||||
|         {:error, %Changeset{} = changeset} -> | ||||
|           socket |> assign(:changeset, changeset) | ||||
|       end | ||||
|  | ||||
|     {:noreply, socket} | ||||
|   end | ||||
|  | ||||
|   defp save_invite( | ||||
|          %{assigns: %{current_user: current_user, return_to: return_to}} = socket, | ||||
|          :new, | ||||
|          invite_params | ||||
|        ) do | ||||
|     socket = | ||||
|       case current_user |> Invites.create_invite(invite_params) do | ||||
|         {:ok, %{name: invite_name}} -> | ||||
|           prompt = | ||||
|             dgettext("prompts", "%{invite_name} created successfully", invite_name: invite_name) | ||||
|  | ||||
|           socket |> put_flash(:info, prompt) |> push_redirect(to: return_to) | ||||
|  | ||||
|         {:error, %Changeset{} = changeset} -> | ||||
|           socket |> assign(changeset: changeset) | ||||
|       end | ||||
|  | ||||
|     {:noreply, socket} | ||||
|   end | ||||
| end | ||||
							
								
								
									
										33
									
								
								lib/memex_web/live/invite_live/form_component.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								lib/memex_web/live/invite_live/form_component.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| <div> | ||||
|   <h2 class="mb-8 text-center title text-xl text-primary-600"> | ||||
|     <%= @title %> | ||||
|   </h2> | ||||
|   <.form | ||||
|     let={f} | ||||
|     for={@changeset} | ||||
|     id="invite-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} | ||||
|     phx-change="validate" | ||||
|     phx-submit="save" | ||||
|   > | ||||
|     <%= if @changeset.action && not @changeset.valid? do %> | ||||
|       <div class="invalid-feedback col-span-3 text-center"> | ||||
|         <%= changeset_errors(@changeset) %> | ||||
|       </div> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= label(f, :name, gettext("Name"), class: "title text-lg text-primary-600") %> | ||||
|     <%= text_input(f, :name, class: "input input-primary col-span-2") %> | ||||
|     <%= error_tag(f, :name, "col-span-3") %> | ||||
|  | ||||
|     <%= label(f, :uses_left, gettext("Uses left"), class: "title text-lg text-primary-600") %> | ||||
|     <%= number_input(f, :uses_left, min: 0, class: "input input-primary col-span-2") %> | ||||
|     <%= error_tag(f, :uses_left, "col-span-3") %> | ||||
|  | ||||
|     <%= submit(dgettext("actions", "Save"), | ||||
|       class: "mx-auto btn btn-primary col-span-3", | ||||
|       phx_disable_with: dgettext("prompts", "Saving...") | ||||
|     ) %> | ||||
|   </.form> | ||||
| </div> | ||||
							
								
								
									
										156
									
								
								lib/memex_web/live/invite_live/index.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								lib/memex_web/live/invite_live/index.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| defmodule MemexWeb.InviteLive.Index do | ||||
|   @moduledoc """ | ||||
|   Liveview to show a Memex.Invites.Invite index | ||||
|   """ | ||||
|  | ||||
|   use MemexWeb, :live_view | ||||
|   import MemexWeb.Components.{InviteCard, UserCard} | ||||
|   alias Memex.{Accounts, Invites, Invites.Invite} | ||||
|   alias MemexWeb.{Endpoint, HomeLive} | ||||
|   alias Phoenix.LiveView.JS | ||||
|  | ||||
|   @impl true | ||||
|   def mount(_params, _session, %{assigns: %{current_user: current_user}} = socket) do | ||||
|     socket = | ||||
|       if current_user |> Map.get(:role) == :admin do | ||||
|         socket |> display_invites() | ||||
|       else | ||||
|         prompt = dgettext("errors", "You are not authorized to view this page") | ||||
|         return_to = Routes.live_path(Endpoint, HomeLive) | ||||
|         socket |> put_flash(:error, prompt) |> push_redirect(to: return_to) | ||||
|       end | ||||
|  | ||||
|     {:ok, socket} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do | ||||
|     {:noreply, socket |> apply_action(live_action, params)} | ||||
|   end | ||||
|  | ||||
|   defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do | ||||
|     socket | ||||
|     |> assign(page_title: gettext("Edit Invite"), invite: Invites.get_invite!(id, current_user)) | ||||
|   end | ||||
|  | ||||
|   defp apply_action(socket, :new, _params) do | ||||
|     socket |> assign(page_title: gettext("New Invite"), invite: %Invite{}) | ||||
|   end | ||||
|  | ||||
|   defp apply_action(socket, :index, _params) do | ||||
|     socket |> assign(page_title: gettext("Invites"), invite: nil) | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event( | ||||
|         "delete_invite", | ||||
|         %{"id" => id}, | ||||
|         %{assigns: %{current_user: current_user}} = socket | ||||
|       ) do | ||||
|     %{name: invite_name} = | ||||
|       id |> Invites.get_invite!(current_user) |> Invites.delete_invite!(current_user) | ||||
|  | ||||
|     prompt = dgettext("prompts", "%{invite_name} deleted succesfully", invite_name: invite_name) | ||||
|     {:noreply, socket |> put_flash(:info, prompt) |> display_invites()} | ||||
|   end | ||||
|  | ||||
|   def handle_event( | ||||
|         "set_unlimited", | ||||
|         %{"id" => id}, | ||||
|         %{assigns: %{current_user: current_user}} = socket | ||||
|       ) do | ||||
|     socket = | ||||
|       Invites.get_invite!(id, current_user) | ||||
|       |> Invites.update_invite(%{"uses_left" => nil}, current_user) | ||||
|       |> case do | ||||
|         {:ok, %{name: invite_name}} -> | ||||
|           prompt = | ||||
|             dgettext("prompts", "%{invite_name} updated succesfully", invite_name: invite_name) | ||||
|  | ||||
|           socket |> put_flash(:info, prompt) |> display_invites() | ||||
|  | ||||
|         {:error, changeset} -> | ||||
|           socket |> put_flash(:error, changeset |> changeset_errors()) | ||||
|       end | ||||
|  | ||||
|     {:noreply, socket} | ||||
|   end | ||||
|  | ||||
|   def handle_event( | ||||
|         "enable_invite", | ||||
|         %{"id" => id}, | ||||
|         %{assigns: %{current_user: current_user}} = socket | ||||
|       ) do | ||||
|     socket = | ||||
|       Invites.get_invite!(id, current_user) | ||||
|       |> Invites.update_invite(%{"uses_left" => nil, "disabled_at" => nil}, current_user) | ||||
|       |> case do | ||||
|         {:ok, %{name: invite_name}} -> | ||||
|           prompt = | ||||
|             dgettext("prompts", "%{invite_name} enabled succesfully", invite_name: invite_name) | ||||
|  | ||||
|           socket |> put_flash(:info, prompt) |> display_invites() | ||||
|  | ||||
|         {:error, changeset} -> | ||||
|           socket |> put_flash(:error, changeset |> changeset_errors()) | ||||
|       end | ||||
|  | ||||
|     {:noreply, socket} | ||||
|   end | ||||
|  | ||||
|   def handle_event( | ||||
|         "disable_invite", | ||||
|         %{"id" => id}, | ||||
|         %{assigns: %{current_user: current_user}} = socket | ||||
|       ) do | ||||
|     now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) | ||||
|  | ||||
|     socket = | ||||
|       Invites.get_invite!(id, current_user) | ||||
|       |> Invites.update_invite(%{"uses_left" => 0, "disabled_at" => now}, current_user) | ||||
|       |> case do | ||||
|         {:ok, %{name: invite_name}} -> | ||||
|           prompt = | ||||
|             dgettext("prompts", "%{invite_name} disabled succesfully", invite_name: invite_name) | ||||
|  | ||||
|           socket |> put_flash(:info, prompt) |> display_invites() | ||||
|  | ||||
|         {:error, changeset} -> | ||||
|           socket |> put_flash(:error, changeset |> changeset_errors()) | ||||
|       end | ||||
|  | ||||
|     {:noreply, socket} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event("copy_to_clipboard", _, socket) do | ||||
|     prompt = dgettext("prompts", "Copied to clipboard") | ||||
|     {:noreply, socket |> put_flash(:info, prompt)} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event( | ||||
|         "delete_user", | ||||
|         %{"id" => id}, | ||||
|         %{assigns: %{current_user: current_user}} = socket | ||||
|       ) do | ||||
|     %{email: user_email} = Accounts.get_user!(id) |> Accounts.delete_user!(current_user) | ||||
|  | ||||
|     prompt = dgettext("prompts", "%{user_email} deleted succesfully", user_email: user_email) | ||||
|  | ||||
|     {:noreply, socket |> put_flash(:info, prompt) |> display_invites()} | ||||
|   end | ||||
|  | ||||
|   defp display_invites(%{assigns: %{current_user: current_user}} = socket) do | ||||
|     invites = Invites.list_invites(current_user) | ||||
|     all_users = Accounts.list_all_users_by_role(current_user) | ||||
|  | ||||
|     admins = | ||||
|       all_users | ||||
|       |> Map.get(:admin, []) | ||||
|       |> Enum.reject(fn %{id: user_id} -> user_id == current_user.id end) | ||||
|  | ||||
|     users = all_users |> Map.get(:user, []) | ||||
|     socket |> assign(invites: invites, admins: admins, users: users) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										156
									
								
								lib/memex_web/live/invite_live/index.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								lib/memex_web/live/invite_live/index.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| <div class="w-full flex flex-col space-y-8 justify-center items-center"> | ||||
|   <h1 class="title text-2xl title-primary-500"> | ||||
|     <%= gettext("Invites") %> | ||||
|   </h1> | ||||
|  | ||||
|   <%= if @invites |> Enum.empty?() do %> | ||||
|     <h1 class="title text-xl text-primary-600"> | ||||
|       <%= gettext("No invites 😔") %> | ||||
|     </h1> | ||||
|  | ||||
|     <%= live_patch(dgettext("actions", "Invite someone new!"), | ||||
|       to: Routes.invite_index_path(Endpoint, :new), | ||||
|       class: "btn btn-primary" | ||||
|     ) %> | ||||
|   <% else %> | ||||
|     <%= live_patch(dgettext("actions", "Create Invite"), | ||||
|       to: Routes.invite_index_path(Endpoint, :new), | ||||
|       class: "btn btn-primary" | ||||
|     ) %> | ||||
|   <% end %> | ||||
|  | ||||
|   <div class="w-full flex flex-row flex-wrap justify-center items-center"> | ||||
|     <%= for invite <- @invites do %> | ||||
|       <.invite_card invite={invite}> | ||||
|         <:code_actions> | ||||
|           <form phx-submit="copy_to_clipboard"> | ||||
|             <button | ||||
|               type="submit" | ||||
|               class="mx-2 my-1 btn btn-primary" | ||||
|               phx-click={JS.dispatch("memex:clipcopy", to: "#code-#{invite.id}")} | ||||
|             > | ||||
|               <%= dgettext("actions", "Copy to clipboard") %> | ||||
|             </button> | ||||
|           </form> | ||||
|         </:code_actions> | ||||
|         <%= live_patch to: Routes.invite_index_path(Endpoint, :edit, invite), | ||||
|                    class: "text-primary-600 link", | ||||
|                    data: [qa: "edit-#{invite.id}"] do %> | ||||
|           <i class="fa-fw fa-lg fas fa-edit"></i> | ||||
|         <% end %> | ||||
|  | ||||
|         <%= link to: "#", | ||||
|              class: "text-primary-600 link", | ||||
|              phx_click: "delete_invite", | ||||
|              phx_value_id: invite.id, | ||||
|              data: [ | ||||
|                confirm: | ||||
|                  dgettext("prompts", "Are you sure you want to delete the invite for %{invite_name}?", | ||||
|                    invite_name: invite.name | ||||
|                  ), | ||||
|                qa: "delete-#{invite.id}" | ||||
|              ] do %> | ||||
|           <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|         <% end %> | ||||
|  | ||||
|         <%= if invite.disabled_at |> is_nil() do %> | ||||
|           <a href="#" class="btn btn-primary" phx-click="disable_invite" phx-value-id={invite.id}> | ||||
|             <%= gettext("Disable") %> | ||||
|           </a> | ||||
|         <% else %> | ||||
|           <a href="#" class="btn btn-primary" phx-click="enable_invite" phx-value-id={invite.id}> | ||||
|             <%= gettext("Enable") %> | ||||
|           </a> | ||||
|         <% end %> | ||||
|  | ||||
|         <%= if invite.disabled_at |> is_nil() and not (invite.uses_left |> is_nil()) do %> | ||||
|           <a | ||||
|             href="#" | ||||
|             class="btn btn-primary" | ||||
|             phx-click="set_unlimited" | ||||
|             phx-value-id={invite.id} | ||||
|             data-confirm={ | ||||
|               dgettext("prompts", "Are you sure you want to make %{invite_name} unlimited?", | ||||
|                 invite_name: invite.name | ||||
|               ) | ||||
|             } | ||||
|           > | ||||
|             <%= gettext("Set Unlimited") %> | ||||
|           </a> | ||||
|         <% end %> | ||||
|       </.invite_card> | ||||
|     <% end %> | ||||
|   </div> | ||||
|  | ||||
|   <%= unless @admins |> Enum.empty?() do %> | ||||
|     <hr class="hr" /> | ||||
|  | ||||
|     <h1 class="title text-2xl text-primary-600"> | ||||
|       <%= gettext("Admins") %> | ||||
|     </h1> | ||||
|  | ||||
|     <div class="w-full flex flex-row flex-wrap justify-center items-center"> | ||||
|       <%= for admin <- @admins do %> | ||||
|         <.user_card user={admin}> | ||||
|           <%= link to: "#", | ||||
|                class: "text-primary-600 link", | ||||
|                phx_click: "delete_user", | ||||
|                phx_value_id: admin.id, | ||||
|                data: [ | ||||
|                  confirm: | ||||
|                    dgettext( | ||||
|                      "prompts", | ||||
|                      "Are you sure you want to delete %{email}? This action is permanent!", | ||||
|                      email: admin.email | ||||
|                    ) | ||||
|                ] do %> | ||||
|             <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|           <% end %> | ||||
|         </.user_card> | ||||
|       <% end %> | ||||
|     </div> | ||||
|   <% end %> | ||||
|  | ||||
|   <%= unless @users |> Enum.empty?() do %> | ||||
|     <hr class="hr" /> | ||||
|  | ||||
|     <h1 class="title text-2xl text-primary-600"> | ||||
|       <%= gettext("Users") %> | ||||
|     </h1> | ||||
|  | ||||
|     <div class="w-full flex flex-row flex-wrap justify-center items-center"> | ||||
|       <%= for user <- @users do %> | ||||
|         <.user_card user={user}> | ||||
|           <%= link to: "#", | ||||
|                class: "text-primary-600 link", | ||||
|                phx_click: "delete_user", | ||||
|                phx_value_id: user.id, | ||||
|                data: [ | ||||
|                  confirm: | ||||
|                    dgettext( | ||||
|                      "prompts", | ||||
|                      "Are you sure you want to delete %{email}? This action is permanent!", | ||||
|                      email: user.email | ||||
|                    ) | ||||
|                ] do %> | ||||
|             <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|           <% end %> | ||||
|         </.user_card> | ||||
|       <% end %> | ||||
|     </div> | ||||
|   <% end %> | ||||
| </div> | ||||
|  | ||||
| <%= if @live_action in [:new, :edit] do %> | ||||
|   <.modal return_to={Routes.invite_index_path(Endpoint, :index)}> | ||||
|     <.live_component | ||||
|       module={MemexWeb.InviteLive.FormComponent} | ||||
|       id={@invite.id || :new} | ||||
|       title={@page_title} | ||||
|       action={@live_action} | ||||
|       invite={@invite} | ||||
|       return_to={Routes.invite_index_path(Endpoint, :index)} | ||||
|       current_user={@current_user} | ||||
|     /> | ||||
|   </.modal> | ||||
| <% end %> | ||||
							
								
								
									
										80
									
								
								lib/memex_web/live/live_helpers.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								lib/memex_web/live/live_helpers.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| defmodule MemexWeb.LiveHelpers do | ||||
|   @moduledoc """ | ||||
|   Contains resuable methods for all liveviews | ||||
|   """ | ||||
|  | ||||
|   import Phoenix.LiveView.Helpers | ||||
|   alias Phoenix.LiveView.JS | ||||
|  | ||||
|   @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""" | ||||
|     <%= live_patch to: @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() | ||||
|     do %> | ||||
|       <span class="hidden"></span> | ||||
|     <% end %> | ||||
|  | ||||
|     <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-center items-center | ||||
|         flex flex-col justify-start items-center | ||||
|         bg-white border-2 rounded-lg" | ||||
|       > | ||||
|         <%= live_patch to: @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() do %> | ||||
|           <i class="fa-fw fa-lg fas fa-times"></i> | ||||
|         <% end %> | ||||
|  | ||||
|         <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 | ||||
|  | ||||
|   def 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 | ||||
| end | ||||
							
								
								
									
										99
									
								
								lib/memex_web/router.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								lib/memex_web/router.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| defmodule MemexWeb.Router do | ||||
|   use MemexWeb, :router | ||||
|   import Phoenix.LiveDashboard.Router | ||||
|   import MemexWeb.UserAuth | ||||
|  | ||||
|   pipeline :browser do | ||||
|     plug :accepts, ["html"] | ||||
|     plug :fetch_session | ||||
|     plug :fetch_live_flash | ||||
|     plug :put_root_layout, {MemexWeb.LayoutView, :root} | ||||
|     plug :protect_from_forgery | ||||
|     plug :put_secure_browser_headers | ||||
|     plug :fetch_current_user | ||||
|     plug :put_user_locale, default: Application.get_env(:gettext, :default_locale, "en_US") | ||||
|   end | ||||
|  | ||||
|   defp put_user_locale(%{assigns: %{current_user: %{locale: locale}}} = conn, default: default) do | ||||
|     Gettext.put_locale(locale || default) | ||||
|     conn |> put_session(:locale, locale || default) | ||||
|   end | ||||
|  | ||||
|   defp put_user_locale(conn, default: default) do | ||||
|     Gettext.put_locale(default) | ||||
|     conn |> put_session(:locale, default) | ||||
|   end | ||||
|  | ||||
|   pipeline :require_admin do | ||||
|     plug :require_role, role: :admin | ||||
|   end | ||||
|  | ||||
|   pipeline :api do | ||||
|     plug :accepts, ["json"] | ||||
|   end | ||||
|  | ||||
|   scope "/", MemexWeb do | ||||
|     pipe_through :browser | ||||
|  | ||||
|     live "/", HomeLive | ||||
|   end | ||||
|  | ||||
|   ## Authentication routes | ||||
|  | ||||
|   scope "/", MemexWeb do | ||||
|     pipe_through [:browser, :redirect_if_user_is_authenticated] | ||||
|  | ||||
|     get "/users/register", UserRegistrationController, :new | ||||
|     post "/users/register", UserRegistrationController, :create | ||||
|     get "/users/log_in", UserSessionController, :new | ||||
|     post "/users/log_in", UserSessionController, :create | ||||
|     get "/users/reset_password", UserResetPasswordController, :new | ||||
|     post "/users/reset_password", UserResetPasswordController, :create | ||||
|     get "/users/reset_password/:token", UserResetPasswordController, :edit | ||||
|     put "/users/reset_password/:token", UserResetPasswordController, :update | ||||
|   end | ||||
|  | ||||
|   scope "/", MemexWeb do | ||||
|     pipe_through [:browser, :require_authenticated_user] | ||||
|  | ||||
|     get "/users/settings", UserSettingsController, :edit | ||||
|     put "/users/settings", UserSettingsController, :update | ||||
|     delete "/users/settings/:id", UserSettingsController, :delete | ||||
|     get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email | ||||
|   end | ||||
|  | ||||
|   scope "/", MemexWeb do | ||||
|     pipe_through [:browser, :require_authenticated_user, :require_admin] | ||||
|  | ||||
|     live_dashboard "/dashboard", metrics: MemexWeb.Telemetry, ecto_repos: [Memex.Repo] | ||||
|  | ||||
|     live "/invites", InviteLive.Index, :index | ||||
|     live "/invites/new", InviteLive.Index, :new | ||||
|     live "/invites/:id/edit", InviteLive.Index, :edit | ||||
|   end | ||||
|  | ||||
|   scope "/", MemexWeb do | ||||
|     pipe_through [:browser] | ||||
|  | ||||
|     delete "/users/log_out", UserSessionController, :delete | ||||
|     get "/users/confirm", UserConfirmationController, :new | ||||
|     post "/users/confirm", UserConfirmationController, :create | ||||
|     get "/users/confirm/:token", UserConfirmationController, :confirm | ||||
|   end | ||||
|  | ||||
|   # Enables the Swoosh mailbox preview in development. | ||||
|   # | ||||
|   # Note that preview only shows emails that were sent by the same | ||||
|   # node running the Phoenix server. | ||||
|   if Mix.env() == :dev do | ||||
|     scope "/dev" do | ||||
|       pipe_through :browser | ||||
|  | ||||
|       forward "/mailbox", Plug.Swoosh.MailboxPreview | ||||
|     end | ||||
|  | ||||
|     scope "/dev" do | ||||
|       get "/preview/:id", MemexWeb.EmailController, :preview | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										75
									
								
								lib/memex_web/telemetry.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								lib/memex_web/telemetry.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| defmodule MemexWeb.Telemetry do | ||||
|   @moduledoc """ | ||||
|   Collects telemetry | ||||
|   """ | ||||
|  | ||||
|   use Supervisor | ||||
|   import Telemetry.Metrics | ||||
|  | ||||
|   def start_link(arg) do | ||||
|     Supervisor.start_link(__MODULE__, arg, name: __MODULE__) | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def init(_arg) do | ||||
|     children = [ | ||||
|       # Telemetry poller will execute the given period measurements | ||||
|       # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics | ||||
|       {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} | ||||
|       # Add reporters as children of your supervision tree. | ||||
|       # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} | ||||
|     ] | ||||
|  | ||||
|     Supervisor.init(children, strategy: :one_for_one) | ||||
|   end | ||||
|  | ||||
|   def metrics do | ||||
|     [ | ||||
|       # Phoenix Metrics | ||||
|       summary("phoenix.endpoint.stop.duration", | ||||
|         unit: {:native, :millisecond} | ||||
|       ), | ||||
|       summary("phoenix.router_dispatch.stop.duration", | ||||
|         tags: [:route], | ||||
|         unit: {:native, :millisecond} | ||||
|       ), | ||||
|  | ||||
|       # Database Metrics | ||||
|       summary("memex.repo.query.total_time", | ||||
|         unit: {:native, :millisecond}, | ||||
|         description: "The sum of the other measurements" | ||||
|       ), | ||||
|       summary("memex.repo.query.decode_time", | ||||
|         unit: {:native, :millisecond}, | ||||
|         description: "The time spent decoding the data received from the database" | ||||
|       ), | ||||
|       summary("memex.repo.query.query_time", | ||||
|         unit: {:native, :millisecond}, | ||||
|         description: "The time spent executing the query" | ||||
|       ), | ||||
|       summary("memex.repo.query.queue_time", | ||||
|         unit: {:native, :millisecond}, | ||||
|         description: "The time spent waiting for a database connection" | ||||
|       ), | ||||
|       summary("memex.repo.query.idle_time", | ||||
|         unit: {:native, :millisecond}, | ||||
|         description: | ||||
|           "The time the connection spent waiting before being checked out for the query" | ||||
|       ), | ||||
|  | ||||
|       # VM Metrics | ||||
|       summary("vm.memory.total", unit: {:byte, :kilobyte}), | ||||
|       summary("vm.total_run_queue_lengths.total"), | ||||
|       summary("vm.total_run_queue_lengths.cpu"), | ||||
|       summary("vm.total_run_queue_lengths.io") | ||||
|     ] | ||||
|   end | ||||
|  | ||||
|   defp periodic_measurements do | ||||
|     [ | ||||
|       # A module, function and arguments to be invoked periodically. | ||||
|       # This function must call :telemetry.execute/3 and a metric must be added above. | ||||
|       # {MemexWeb, :count_users, []} | ||||
|     ] | ||||
|   end | ||||
| end | ||||
							
								
								
									
										23
									
								
								lib/memex_web/templates/email/confirm_email.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								lib/memex_web/templates/email/confirm_email.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| <div style="display: flex; flex-direction: column; justify-content: center; align-items: center;"> | ||||
|   <span style="margin-bottom: 0.75em; font-size: 1.5em;"> | ||||
|     <%= dgettext("emails", "Hi %{email},", email: @user.email) %> | ||||
|   </span> | ||||
|  | ||||
|   <br /> | ||||
|  | ||||
|   <span style="margin-bottom: 1em; font-size: 1.25em;"> | ||||
|     <%= dgettext("emails", "Welcome to Memex") %> | ||||
|   </span> | ||||
|  | ||||
|   <br /> | ||||
|  | ||||
|   <%= dgettext("emails", "You can confirm your account by visiting the URL below:") %> | ||||
|  | ||||
|   <br /> | ||||
|  | ||||
|   <a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a> | ||||
|  | ||||
|   <br /> | ||||
|  | ||||
|   <%= dgettext("emails", "If you didn't create an account at Memex, please ignore this.") %> | ||||
| </div> | ||||
							
								
								
									
										12
									
								
								lib/memex_web/templates/email/confirm_email.txt.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								lib/memex_web/templates/email/confirm_email.txt.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
|  | ||||
| <%= dgettext("emails", "Hi %{email},", email: @user.email) %> | ||||
|  | ||||
| <%= dgettext("emails", "Welcome to Memex") %> | ||||
|  | ||||
| <%= dgettext("emails", "You can confirm your account by visiting the URL below:") %> | ||||
|  | ||||
| <%= @url %> | ||||
|  | ||||
| <%= dgettext("emails", | ||||
|   "If you didn't create an account at %{url}, please ignore this.", | ||||
|   url: Routes.live_url(Endpoint, HomeLive)) %> | ||||
							
								
								
									
										17
									
								
								lib/memex_web/templates/email/reset_password.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/memex_web/templates/email/reset_password.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| <div style="display: flex; flex-direction: column; justify-content: center; align-items: center;"> | ||||
|   <span style="margin-bottom: 0.5em; font-size: 1.5em;"> | ||||
|     <%= dgettext("emails", "Hi %{email},", email: @user.email) %> | ||||
|   </span> | ||||
|  | ||||
|   <br /> | ||||
|  | ||||
|   <%= dgettext("emails", "You can reset your password by visiting the URL below:") %> | ||||
|  | ||||
|   <br /> | ||||
|  | ||||
|   <a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a> | ||||
|  | ||||
|   <br /> | ||||
|  | ||||
|   <%= dgettext("emails", "If you didn't request this change from Memex, please ignore this.") %> | ||||
| </div> | ||||
							
								
								
									
										10
									
								
								lib/memex_web/templates/email/reset_password.txt.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								lib/memex_web/templates/email/reset_password.txt.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
|  | ||||
| <%= dgettext("emails", "Hi %{email},", email: @user.email) %> | ||||
|  | ||||
| <%= dgettext("emails", "You can reset your password by visiting the URL below:") %> | ||||
|  | ||||
| <%= @url %> | ||||
|  | ||||
| <%= dgettext("emails", | ||||
|   "If you didn't request this change from %{url}, please ignore this.", | ||||
|   url: Routes.live_url(Endpoint, HomeLive)) %> | ||||
							
								
								
									
										20
									
								
								lib/memex_web/templates/email/update_email.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								lib/memex_web/templates/email/update_email.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| <div style="display: flex; flex-direction: column; justify-content: center; align-items: center;"> | ||||
|   <span style="margin-bottom: 0.5em; font-size: 1.5em;"> | ||||
|     <%= dgettext("emails", "Hi %{email},", email: @user.email) %> | ||||
|   </span> | ||||
|  | ||||
|   <br /> | ||||
|  | ||||
|   <%= dgettext("emails", "You can change your email by visiting the URL below:") %> | ||||
|  | ||||
|   <br /> | ||||
|  | ||||
|   <a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a> | ||||
|  | ||||
|   <br /> | ||||
|  | ||||
|   <%= dgettext( | ||||
|     "emails", | ||||
|     "If you didn't request this change from Memex, please ignore this." | ||||
|   ) %> | ||||
| </div> | ||||
							
								
								
									
										10
									
								
								lib/memex_web/templates/email/update_email.txt.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								lib/memex_web/templates/email/update_email.txt.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
|  | ||||
| <%= dgettext("emails", "Hi %{email},", email: @user.email) %> | ||||
|  | ||||
| <%= dgettext("emails", "You can change your email by visiting the URL below:") %> | ||||
|  | ||||
| <%= @url %> | ||||
|  | ||||
| <%= dgettext("emails", | ||||
|   "If you didn't request this change from %{url}, please ignore this.", | ||||
|   url: Routes.live_url(Endpoint, HomeLive)) %> | ||||
							
								
								
									
										33
									
								
								lib/memex_web/templates/error/error.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								lib/memex_web/templates/error/error.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en" class="m-0 p-0 w-full h-full"> | ||||
|   <head> | ||||
|     <meta charset="utf-8" /> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title> | ||||
|       <%= dgettext("errors", "Error") %>| Memex | ||||
|     </title> | ||||
|     <link rel="stylesheet" href="/css/app.css" /> | ||||
|     <script defer type="text/javascript" src="/js/app.js"> | ||||
|     </script> | ||||
|   </head> | ||||
|   <body class="pb-8 m-0 p-0 w-full h-full"> | ||||
|     <header> | ||||
|       <.topbar current_user={assigns[:current_user]}></.topbar> | ||||
|     </header> | ||||
|  | ||||
|     <div class="pb-8 w-full flex flex-col justify-center items-center text-center"> | ||||
|       <div class="p-8 sm:p-16 w-full flex flex-col justify-center items-center space-y-4 max-w-3xl"> | ||||
|         <h1 class="title text-primary-600 text-3xl"> | ||||
|           <%= @error_string %> | ||||
|         </h1> | ||||
|  | ||||
|         <hr class="w-full hr" /> | ||||
|  | ||||
|         <a href={Routes.live_path(Endpoint, HomeLive)} class="link title text-primary-600 text-lg"> | ||||
|           <%= dgettext("errors", "Go back home") %> | ||||
|         </a> | ||||
|       </div> | ||||
|     </div> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										22
									
								
								lib/memex_web/templates/layout/app.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								lib/memex_web/templates/layout/app.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| <main role="main" class="min-h-full min-w-full"> | ||||
|   <header> | ||||
|     <.topbar current_user={assigns[:current_user]}></.topbar> | ||||
|  | ||||
|     <div class="mx-8 my-2 flex flex-col space-y-4 text-center"> | ||||
|       <%= if get_flash(@conn, :info) do %> | ||||
|         <p class="alert alert-info" role="alert"> | ||||
|           <%= get_flash(@conn, :info) %> | ||||
|         </p> | ||||
|       <% end %> | ||||
|       <%= if get_flash(@conn, :error) do %> | ||||
|         <p class="alert alert-danger" role="alert"> | ||||
|           <%= get_flash(@conn, :error) %> | ||||
|         </p> | ||||
|       <% end %> | ||||
|     </div> | ||||
|   </header> | ||||
|  | ||||
|   <div class="mx-4 sm:mx-8 md:mx-16"> | ||||
|     <%= @inner_content %> | ||||
|   </div> | ||||
| </main> | ||||
							
								
								
									
										19
									
								
								lib/memex_web/templates/layout/email.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								lib/memex_web/templates/layout/email.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| <html> | ||||
|   <head> | ||||
|     <title> | ||||
|       <%= @email.subject %> | ||||
|     </title> | ||||
|   </head> | ||||
|   <body style="padding: 2em; color: rgb(31, 31, 31); background-color: rgb(220, 220, 228); font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; text-align: center;"> | ||||
|     <%= @inner_content %> | ||||
|  | ||||
|     <hr style="margin: 2em auto; border-width: 1px; border-color: rgb(212, 212, 216); width: 100%; max-width: 42rem;" /> | ||||
|  | ||||
|     <a style="color: rgb(31, 31, 31);" href={Routes.live_url(Endpoint, HomeLive)}> | ||||
|       <%= dgettext( | ||||
|         "emails", | ||||
|         "This email was sent from Memex, the self-hosted firearm tracker website." | ||||
|       ) %> | ||||
|     </a> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										11
									
								
								lib/memex_web/templates/layout/email.txt.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								lib/memex_web/templates/layout/email.txt.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| <%= @email.subject %> | ||||
|  | ||||
| ==================== | ||||
|  | ||||
| <%= @inner_content %> | ||||
|  | ||||
| ===================== | ||||
|  | ||||
| <%= dgettext("emails", | ||||
|   "This email was sent from Memex at %{url}, the self-hosted firearm tracker website.", | ||||
|   url: Routes.live_url(Endpoint, HomeLive)) %> | ||||
							
								
								
									
										1
									
								
								lib/memex_web/templates/layout/empty.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								lib/memex_web/templates/layout/empty.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <%= @inner_content %> | ||||
							
								
								
									
										54
									
								
								lib/memex_web/templates/layout/live.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								lib/memex_web/templates/layout/live.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| <main class="mb-8 min-w-full"> | ||||
|   <header> | ||||
|     <.topbar current_user={assigns[:current_user]}></.topbar> | ||||
|  | ||||
|     <div class="mx-8 my-2 flex flex-col space-y-4 text-center"> | ||||
|       <%= if @flash && @flash |> Map.has_key?("info") do %> | ||||
|         <p class="alert alert-info" role="alert" phx-click="lv:clear-flash" phx-value-key="info"> | ||||
|           <%= live_flash(@flash, "info") %> | ||||
|         </p> | ||||
|       <% end %> | ||||
|  | ||||
|       <%= if @flash && @flash |> Map.has_key?("error") do %> | ||||
|         <p | ||||
|           class="alert alert-danger" | ||||
|           role="alert" | ||||
|           phx-click="lv:clear-flash" | ||||
|           phx-value-key="error" | ||||
|         > | ||||
|           <%= live_flash(@flash, "error") %> | ||||
|         </p> | ||||
|       <% end %> | ||||
|     </div> | ||||
|   </header> | ||||
|  | ||||
|   <div class="mx-4 sm:mx-8 md:mx-16"> | ||||
|     <%= @inner_content %> | ||||
|   </div> | ||||
| </main> | ||||
|  | ||||
| <div | ||||
|   id="loading" | ||||
|   class="fixed opacity-0 top-0 left-0 w-screen h-screen bg-white z-50 | ||||
|   flex flex-col justify-center items-center space-y-4 | ||||
|   transition-opacity ease-in-out duration-500" | ||||
| > | ||||
|   <h1 class="title text-2xl title-primary-500 text-primary-500"> | ||||
|     <%= gettext("Loading...") %> | ||||
|   </h1> | ||||
|  | ||||
|   <i class="fas fa-3x fa-spin fa-gear text-primary-500"></i> | ||||
| </div> | ||||
|  | ||||
| <div | ||||
|   id="disconnect" | ||||
|   class="fixed opacity-0 top-0 left-0 w-screen h-screen bg-white z-50 | ||||
|   flex flex-col justify-center items-center space-y-4 | ||||
|   transition-opacity ease-in-out duration-500" | ||||
| > | ||||
|   <h1 class="title text-2xl title-primary-500 text-primary-500"> | ||||
|     <%= gettext("Reconnecting...") %> | ||||
|   </h1> | ||||
|  | ||||
|   <i class="fas fa-3x fa-fade fa-satellite-dish text-primary-500"></i> | ||||
| </div> | ||||
							
								
								
									
										23
									
								
								lib/memex_web/templates/layout/root.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								lib/memex_web/templates/layout/root.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en" class="m-0 p-0 w-full h-full"> | ||||
|   <head> | ||||
|     <meta charset="utf-8" /> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <%= csrf_meta_tag() %> | ||||
|     <%= if(assigns |> Map.has_key?(:page_title), do: @page_title, else: "Memex") | ||||
|     |> live_title_tag(suffix: " | Memex") %> | ||||
|     <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} /> | ||||
|     <script | ||||
|       defer | ||||
|       phx-track-static | ||||
|       type="text/javascript" | ||||
|       src={Routes.static_path(@conn, "/js/app.js")} | ||||
|     > | ||||
|     </script> | ||||
|   </head> | ||||
|  | ||||
|   <body class="m-0 p-0 w-full h-full"> | ||||
|     <%= @inner_content %> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										34
									
								
								lib/memex_web/templates/user_confirmation/new.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								lib/memex_web/templates/user_confirmation/new.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| <div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center space-y-4"> | ||||
|   <h1 class="title text-primary-600 text-xl"> | ||||
|     <%= dgettext("actions", "Resend confirmation instructions") %> | ||||
|   </h1> | ||||
|  | ||||
|   <.form | ||||
|     let={f} | ||||
|     for={: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" | ||||
|   > | ||||
|     <%= label(f, :email, class: "title text-lg text-primary-600") %> | ||||
|     <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %> | ||||
|  | ||||
|     <%= submit(dgettext("actions", "Resend confirmation instructions"), | ||||
|       class: "mx-auto btn btn-primary col-span-3" | ||||
|     ) %> | ||||
|   </.form> | ||||
|  | ||||
|   <hr class="hr" /> | ||||
|  | ||||
|   <div class="flex flex-row justify-center items-center space-x-4"> | ||||
|     <%= if Accounts.allow_registration?() do %> | ||||
|       <%= link(dgettext("actions", "Register"), | ||||
|         to: Routes.user_registration_path(@conn, :new), | ||||
|         class: "btn btn-primary" | ||||
|       ) %> | ||||
|     <% end %> | ||||
|     <%= link(dgettext("actions", "Log in"), | ||||
|       to: Routes.user_session_path(@conn, :new), | ||||
|       class: "btn btn-primary" | ||||
|     ) %> | ||||
|   </div> | ||||
| </div> | ||||
							
								
								
									
										56
									
								
								lib/memex_web/templates/user_registration/new.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								lib/memex_web/templates/user_registration/new.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| <div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center space-y-4"> | ||||
|   <h1 class="title text-primary-600 text-xl"> | ||||
|     <%= dgettext("actions", "Register") %> | ||||
|   </h1> | ||||
|  | ||||
|   <.form | ||||
|     let={f} | ||||
|     for={@changeset} | ||||
|     action={Routes.user_registration_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" | ||||
|   > | ||||
|     <%= if @changeset.action && not @changeset.valid? do %> | ||||
|       <div class="alert alert-danger col-span-3"> | ||||
|         <p> | ||||
|           <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> | ||||
|         </p> | ||||
|       </div> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= if @invite do %> | ||||
|       <%= hidden_input(f, :invite_token, value: @invite.token) %> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= label(f, :email, class: "title text-lg text-primary-600") %> | ||||
|     <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %> | ||||
|     <%= error_tag(f, :email, "col-span-3") %> | ||||
|  | ||||
|     <%= label(f, :password, class: "title text-lg text-primary-600") %> | ||||
|     <%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %> | ||||
|     <%= error_tag(f, :password, "col-span-3") %> | ||||
|  | ||||
|     <%= label(f, :locale, gettext("Language"), class: "title text-lg text-primary-600") %> | ||||
|     <%= select( | ||||
|       f, | ||||
|       :locale, | ||||
|       [{gettext("English"), "en_US"}], | ||||
|       class: "input input-primary col-span-2" | ||||
|     ) %> | ||||
|     <%= error_tag(f, :locale) %> | ||||
|  | ||||
|     <%= submit(dgettext("actions", "Register"), class: "mx-auto btn btn-primary col-span-3") %> | ||||
|   </.form> | ||||
|  | ||||
|   <hr class="hr" /> | ||||
|  | ||||
|   <div class="flex flex-row justify-center items-center space-x-4"> | ||||
|     <%= link(dgettext("actions", "Log in"), | ||||
|       to: Routes.user_session_path(@conn, :new), | ||||
|       class: "btn btn-primary" | ||||
|     ) %> | ||||
|     <%= link(dgettext("actions", "Forgot your password?"), | ||||
|       to: Routes.user_reset_password_path(@conn, :new), | ||||
|       class: "btn btn-primary" | ||||
|     ) %> | ||||
|   </div> | ||||
| </div> | ||||
							
								
								
									
										52
									
								
								lib/memex_web/templates/user_reset_password/edit.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								lib/memex_web/templates/user_reset_password/edit.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| <div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center space-y-4"> | ||||
|   <h1 class="title text-primary-600 text-xl"> | ||||
|     <%= dgettext("actions", "Reset password") %> | ||||
|   </h1> | ||||
|  | ||||
|   <.form | ||||
|     let={f} | ||||
|     for={@changeset} | ||||
|     action={Routes.user_reset_password_path(@conn, :update, @token)} | ||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||
|   > | ||||
|     <%= if @changeset.action && not @changeset.valid? do %> | ||||
|       <div class="alert alert-danger col-span-3"> | ||||
|         <p> | ||||
|           <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> | ||||
|         </p> | ||||
|       </div> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= label(f, :password, "New password", class: "title text-lg text-primary-600") %> | ||||
|     <%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %> | ||||
|     <%= error_tag(f, :password, "col-span-3") %> | ||||
|  | ||||
|     <%= label(f, :password_confirmation, "Confirm new password", | ||||
|       class: "title text-lg text-primary-600" | ||||
|     ) %> | ||||
|     <%= password_input(f, :password_confirmation, | ||||
|       required: true, | ||||
|       class: "input input-primary col-span-2" | ||||
|     ) %> | ||||
|     <%= error_tag(f, :password_confirmation, "col-span-3") %> | ||||
|  | ||||
|     <%= submit(dgettext("actions", "Reset password"), | ||||
|       class: "mx-auto btn btn-primary col-span-3" | ||||
|     ) %> | ||||
|   </.form> | ||||
|  | ||||
|   <hr class="hr" /> | ||||
|  | ||||
|   <div class="flex flex-row justify-center items-center space-x-4"> | ||||
|     <%= if Accounts.allow_registration?() do %> | ||||
|       <%= link(dgettext("actions", "Register"), | ||||
|         to: Routes.user_registration_path(@conn, :new), | ||||
|         class: "btn btn-primary" | ||||
|       ) %> | ||||
|     <% end %> | ||||
|     <%= link(dgettext("actions", "Log in"), | ||||
|       to: Routes.user_session_path(@conn, :new), | ||||
|       class: "btn btn-primary" | ||||
|     ) %> | ||||
|   </div> | ||||
| </div> | ||||
							
								
								
									
										34
									
								
								lib/memex_web/templates/user_reset_password/new.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								lib/memex_web/templates/user_reset_password/new.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| <div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center space-y-4"> | ||||
|   <h1 class="title text-primary-600 text-xl"> | ||||
|     <%= dgettext("actions", "Forgot your password?") %> | ||||
|   </h1> | ||||
|  | ||||
|   <.form | ||||
|     let={f} | ||||
|     for={: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" | ||||
|   > | ||||
|     <%= label(f, :email, class: "title text-lg text-primary-600") %> | ||||
|     <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %> | ||||
|  | ||||
|     <%= submit(dgettext("actions", "Send instructions to reset password"), | ||||
|       class: "mx-auto btn btn-primary col-span-3" | ||||
|     ) %> | ||||
|   </.form> | ||||
|  | ||||
|   <hr class="hr" /> | ||||
|  | ||||
|   <div class="flex flex-row justify-center items-center space-x-4"> | ||||
|     <%= if Accounts.allow_registration?() do %> | ||||
|       <%= link(dgettext("actions", "Register"), | ||||
|         to: Routes.user_registration_path(@conn, :new), | ||||
|         class: "btn btn-primary" | ||||
|       ) %> | ||||
|     <% end %> | ||||
|     <%= link(dgettext("actions", "Log in"), | ||||
|       to: Routes.user_session_path(@conn, :new), | ||||
|       class: "btn btn-primary" | ||||
|     ) %> | ||||
|   </div> | ||||
| </div> | ||||
							
								
								
									
										49
									
								
								lib/memex_web/templates/user_session/new.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								lib/memex_web/templates/user_session/new.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| <div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center space-y-4"> | ||||
|   <h1 class="title text-primary-600 text-xl"> | ||||
|     <%= dgettext("actions", "Log in") %> | ||||
|   </h1> | ||||
|  | ||||
|   <.form | ||||
|     let={f} | ||||
|     for={@conn} | ||||
|     action={Routes.user_session_path(@conn, :create)} | ||||
|     as="user" | ||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||
|   > | ||||
|     <%= if @error_message do %> | ||||
|       <div class="alert alert-danger col-span-3"> | ||||
|         <p> | ||||
|           <%= @error_message %> | ||||
|         </p> | ||||
|       </div> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= label(f, :email, class: "title text-lg text-primary-600") %> | ||||
|     <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %> | ||||
|  | ||||
|     <%= label(f, :password, class: "title text-lg text-primary-600") %> | ||||
|     <%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %> | ||||
|  | ||||
|     <%= label(f, :remember_me, gettext("Keep me logged in for 60 days"), | ||||
|       class: "title text-lg text-primary-600" | ||||
|     ) %> | ||||
|     <%= checkbox(f, :remember_me, class: "checkbox col-span-2") %> | ||||
|  | ||||
|     <%= submit(dgettext("actions", "Log in"), class: "mx-auto btn btn-primary col-span-3") %> | ||||
|   </.form> | ||||
|  | ||||
|   <hr class="hr" /> | ||||
|  | ||||
|   <div class="flex flex-row justify-center items-center space-x-4"> | ||||
|     <%= if Accounts.allow_registration?() do %> | ||||
|       <%= link(dgettext("actions", "Register"), | ||||
|         to: Routes.user_registration_path(@conn, :new), | ||||
|         class: "btn btn-primary" | ||||
|       ) %> | ||||
|     <% end %> | ||||
|     <%= link(dgettext("actions", "Forgot your password?"), | ||||
|       to: Routes.user_reset_password_path(@conn, :new), | ||||
|       class: "btn btn-primary" | ||||
|     ) %> | ||||
|   </div> | ||||
| </div> | ||||
							
								
								
									
										145
									
								
								lib/memex_web/templates/user_settings/edit.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								lib/memex_web/templates/user_settings/edit.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | ||||
| <div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center text-center space-y-4"> | ||||
|   <h1 class="pb-4 title text-primary-600 text-xl"> | ||||
|     <%= gettext("Settings") %> | ||||
|   </h1> | ||||
|  | ||||
|   <hr class="hr" /> | ||||
|  | ||||
|   <.form | ||||
|     let={f} | ||||
|     for={@email_changeset} | ||||
|     action={Routes.user_settings_path(@conn, :update)} | ||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||
|   > | ||||
|     <h3 class="title text-primary-600 text-lg col-span-3"> | ||||
|       <%= dgettext("actions", "Change email") %> | ||||
|     </h3> | ||||
|  | ||||
|     <%= if @email_changeset.action && not @email_changeset.valid? do %> | ||||
|       <div class="alert alert-danger col-span-3"> | ||||
|         <p> | ||||
|           <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> | ||||
|         </p> | ||||
|       </div> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= hidden_input(f, :action, name: "action", value: "update_email") %> | ||||
|  | ||||
|     <%= label(f, :email, class: "title text-lg text-primary-600") %> | ||||
|     <%= email_input(f, :email, required: true, class: "mx-2 my-1 input input-primary col-span-2") %> | ||||
|     <%= error_tag(f, :email, "col-span-3") %> | ||||
|  | ||||
|     <%= label(f, :current_password, gettext("Current password"), | ||||
|       for: "current_password_for_email", | ||||
|       class: "mx-2 my-1 title text-lg text-primary-600" | ||||
|     ) %> | ||||
|     <%= password_input(f, :current_password, | ||||
|       required: true, | ||||
|       name: "current_password", | ||||
|       id: "current_password_for_email", | ||||
|       class: "mx-2 my-1 input input-primary col-span-2" | ||||
|     ) %> | ||||
|     <%= error_tag(f, :current_password, "col-span-3") %> | ||||
|  | ||||
|     <%= submit(dgettext("actions", "Change email"), | ||||
|       class: "mx-auto btn btn-primary col-span-3" | ||||
|     ) %> | ||||
|   </.form> | ||||
|  | ||||
|   <hr class="hr" /> | ||||
|  | ||||
|   <.form | ||||
|     let={f} | ||||
|     for={@password_changeset} | ||||
|     action={Routes.user_settings_path(@conn, :update)} | ||||
|     class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" | ||||
|   > | ||||
|     <h3 class="title text-primary-600 text-lg col-span-3"> | ||||
|       <%= dgettext("actions", "Change password") %> | ||||
|     </h3> | ||||
|  | ||||
|     <%= if @password_changeset.action && not @password_changeset.valid? do %> | ||||
|       <div class="alert alert-danger col-span-3"> | ||||
|         <p> | ||||
|           <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> | ||||
|         </p> | ||||
|       </div> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= hidden_input(f, :action, name: "action", value: "update_password") %> | ||||
|  | ||||
|     <%= label(f, :password, gettext("New password"), class: "title text-lg text-primary-600") %> | ||||
|     <%= password_input(f, :password, | ||||
|       required: true, | ||||
|       class: "mx-2 my-1 input input-primary col-span-2" | ||||
|     ) %> | ||||
|     <%= error_tag(f, :password, "col-span-3") %> | ||||
|  | ||||
|     <%= label(f, :password_confirmation, gettext("Confirm new password"), | ||||
|       class: "title text-lg text-primary-600" | ||||
|     ) %> | ||||
|     <%= password_input(f, :password_confirmation, | ||||
|       required: true, | ||||
|       class: "mx-2 my-1 input input-primary col-span-2" | ||||
|     ) %> | ||||
|     <%= error_tag(f, :password_confirmation, "col-span-3") %> | ||||
|  | ||||
|     <%= label(f, :current_password, gettext("Current password"), | ||||
|       for: "current_password_for_password", | ||||
|       class: "title text-lg text-primary-600" | ||||
|     ) %> | ||||
|     <%= password_input(f, :current_password, | ||||
|       required: true, | ||||
|       name: "current_password", | ||||
|       id: "current_password_for_password", | ||||
|       class: "mx-2 my-1 input input-primary col-span-2" | ||||
|     ) %> | ||||
|     <%= error_tag(f, :current_password, "col-span-3") %> | ||||
|  | ||||
|     <%= submit(dgettext("actions", "Change password"), | ||||
|       class: "mx-auto btn btn-primary col-span-3" | ||||
|     ) %> | ||||
|   </.form> | ||||
|  | ||||
|   <hr class="hr" /> | ||||
|  | ||||
|   <.form | ||||
|     let={f} | ||||
|     for={@locale_changeset} | ||||
|     action={Routes.user_settings_path(@conn, :update)} | ||||
|     class="flex flex-col space-y-4 justify-center items-center" | ||||
|   > | ||||
|     <h3 class="title text-primary-600 text-lg"> | ||||
|       <%= dgettext("actions", "Change Language") %> | ||||
|     </h3> | ||||
|  | ||||
|     <%= if @locale_changeset.action && not @locale_changeset.valid? do %> | ||||
|       <div class="alert alert-danger"> | ||||
|         <p> | ||||
|           <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> | ||||
|         </p> | ||||
|       </div> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= hidden_input(f, :action, name: "action", value: "update_locale") %> | ||||
|  | ||||
|     <%= select(f, :locale, [{gettext("English"), "en_US"}, {"Spanish", "es"}], | ||||
|       class: "mx-2 my-1 min-w-md input input-primary" | ||||
|     ) %> | ||||
|     <%= error_tag(f, :locale) %> | ||||
|  | ||||
|     <%= submit(dgettext("actions", "Change language"), | ||||
|       class: "whitespace-nowrap mx-auto btn btn-primary", | ||||
|       data: [qa: dgettext("prompts", "Are you sure you want to change your language?")] | ||||
|     ) %> | ||||
|   </.form> | ||||
|  | ||||
|   <hr class="hr" /> | ||||
|  | ||||
|   <%= link(dgettext("actions", "Delete User"), | ||||
|     to: Routes.user_settings_path(@conn, :delete, @current_user), | ||||
|     method: :delete, | ||||
|     class: "btn btn-alert", | ||||
|     data: [confirm: dgettext("prompts", "Are you sure you want to delete your account?")] | ||||
|   ) %> | ||||
| </div> | ||||
							
								
								
									
										8
									
								
								lib/memex_web/views/email_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								lib/memex_web/views/email_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| defmodule MemexWeb.EmailView do | ||||
|   @moduledoc """ | ||||
|   A view for email-related helper functions | ||||
|   """ | ||||
|   alias MemexWeb.{Endpoint, HomeLive} | ||||
|  | ||||
|   use MemexWeb, :view | ||||
| end | ||||
							
								
								
									
										82
									
								
								lib/memex_web/views/error_helpers.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								lib/memex_web/views/error_helpers.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| defmodule MemexWeb.ErrorHelpers do | ||||
|   @moduledoc """ | ||||
|   Conveniences for translating and building error messages. | ||||
|   """ | ||||
|  | ||||
|   use Phoenix.HTML | ||||
|   import Phoenix.LiveView.Helpers | ||||
|   alias Ecto.Changeset | ||||
|   alias Phoenix.{HTML.Form, LiveView.Rendered} | ||||
|  | ||||
|   @doc """ | ||||
|   Generates tag for inlined form input errors. | ||||
|   """ | ||||
|   @spec error_tag(Form.t(), Form.field()) :: Rendered.t() | ||||
|   @spec error_tag(Form.t(), Form.field(), String.t()) :: Rendered.t() | ||||
|   def error_tag(form, field, extra_class \\ "") do | ||||
|     assigns = %{extra_class: extra_class, form: form, field: field} | ||||
|  | ||||
|     ~H""" | ||||
|     <%= for error <- Keyword.get_values(@form.errors, @field) do %> | ||||
|       <span class={"invalid-feedback #{@extra_class}"} phx-feedback-for={input_name(@form, @field)}> | ||||
|         <%= translate_error(error) %> | ||||
|       </span> | ||||
|     <% end %> | ||||
|     """ | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Translates an error message using gettext. | ||||
|   """ | ||||
|   @spec translate_error({String.t(), keyword() | map()}) :: String.t() | ||||
|   def translate_error({msg, opts}) do | ||||
|     # When using gettext, we typically pass the strings we want | ||||
|     # to translate as a static argument: | ||||
|     # | ||||
|     #     # Translate "is invalid" in the "errors" domain | ||||
|     #     dgettext("errors", "is invalid") | ||||
|     # | ||||
|     #     # Translate the number of files with plural rules | ||||
|     #     dngettext("errors", "1 file", "%{count} files", count) | ||||
|     # | ||||
|     # Because the error messages we show in our forms and APIs | ||||
|     # are defined inside Ecto, we need to translate them dynamically. | ||||
|     # This requires us to call the Gettext module passing our gettext | ||||
|     # backend as first argument. | ||||
|     # | ||||
|     # Note we use the "errors" domain, which means translations | ||||
|     # should be written to the errors.po file. The :count option is | ||||
|     # set by Ecto and indicates we should also apply plural rules. | ||||
|     if count = opts[:count] do | ||||
|       Gettext.dngettext(MemexWeb.Gettext, "errors", msg, msg, count, opts) | ||||
|     else | ||||
|       Gettext.dgettext(MemexWeb.Gettext, "errors", msg, opts) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Displays all errors from a changeset, or just for a single key | ||||
|   """ | ||||
|   @spec changeset_errors(Changeset.t()) :: String.t() | ||||
|   @spec changeset_errors(Changeset.t(), key :: atom()) :: [String.t()] | nil | ||||
|   def changeset_errors(changeset) do | ||||
|     changeset | ||||
|     |> changeset_error_map() | ||||
|     |> Enum.map_join(". ", fn {key, errors} -> | ||||
|       "#{key |> humanize()}: #{errors |> Enum.join(", ")}" | ||||
|     end) | ||||
|   end | ||||
|  | ||||
|   def changeset_errors(changeset, key) do | ||||
|     changeset |> changeset_error_map() |> Map.get(key) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Displays all errors from a changeset in a key value map | ||||
|   """ | ||||
|   @spec changeset_error_map(Changeset.t()) :: %{atom() => [String.t()]} | ||||
|   def changeset_error_map(changeset) do | ||||
|     changeset | ||||
|     |> Changeset.traverse_errors(fn error -> error |> translate_error() end) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										16
									
								
								lib/memex_web/views/error_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								lib/memex_web/views/error_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| defmodule MemexWeb.ErrorView do | ||||
|   use MemexWeb, :view | ||||
|   import MemexWeb.Components.Topbar | ||||
|   alias MemexWeb.{Endpoint, HomeLive} | ||||
|  | ||||
|   def template_not_found(error_path, _assigns) do | ||||
|     error_string = | ||||
|       case error_path do | ||||
|         "404.html" -> dgettext("errors", "Not found") | ||||
|         "401.html" -> dgettext("errors", "Unauthorized") | ||||
|         _ -> dgettext("errors", "Internal Server Error") | ||||
|       end | ||||
|  | ||||
|     render("error.html", %{error_string: error_string}) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										3
									
								
								lib/memex_web/views/home_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/memex_web/views/home_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| defmodule MemexWeb.PageView do | ||||
|   use MemexWeb, :view | ||||
| end | ||||
							
								
								
									
										17
									
								
								lib/memex_web/views/layout_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/memex_web/views/layout_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| defmodule MemexWeb.LayoutView do | ||||
|   use MemexWeb, :view | ||||
|   import MemexWeb.Components.Topbar | ||||
|   alias MemexWeb.{Endpoint, HomeLive} | ||||
|  | ||||
|   # Phoenix LiveDashboard is available only in development by default, | ||||
|   # so we instruct Elixir to not warn if the dashboard route is missing. | ||||
|   @compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}} | ||||
|  | ||||
|   def get_title(conn) do | ||||
|     if conn.assigns |> Map.has_key?(:title) do | ||||
|       "Memex | #{conn.assigns.title}" | ||||
|     else | ||||
|       "Memex" | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										4
									
								
								lib/memex_web/views/user_confirmation_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								lib/memex_web/views/user_confirmation_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| defmodule MemexWeb.UserConfirmationView do | ||||
|   use MemexWeb, :view | ||||
|   alias Memex.Accounts | ||||
| end | ||||
							
								
								
									
										3
									
								
								lib/memex_web/views/user_registration_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/memex_web/views/user_registration_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| defmodule MemexWeb.UserRegistrationView do | ||||
|   use MemexWeb, :view | ||||
| end | ||||
							
								
								
									
										4
									
								
								lib/memex_web/views/user_reset_password_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								lib/memex_web/views/user_reset_password_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| defmodule MemexWeb.UserResetPasswordView do | ||||
|   use MemexWeb, :view | ||||
|   alias Memex.Accounts | ||||
| end | ||||
							
								
								
									
										4
									
								
								lib/memex_web/views/user_session_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								lib/memex_web/views/user_session_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| defmodule MemexWeb.UserSessionView do | ||||
|   use MemexWeb, :view | ||||
|   alias Memex.Accounts | ||||
| end | ||||
							
								
								
									
										3
									
								
								lib/memex_web/views/user_settings_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/memex_web/views/user_settings_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| defmodule MemexWeb.UserSettingsView do | ||||
|   use MemexWeb, :view | ||||
| end | ||||
							
								
								
									
										52
									
								
								lib/memex_web/views/view_helpers.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								lib/memex_web/views/view_helpers.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| defmodule MemexWeb.ViewHelpers do | ||||
|   @moduledoc """ | ||||
|   Contains common helpers that can be used in liveviews and regular views. These | ||||
|   are automatically imported into any Phoenix View using `use MemexWeb, | ||||
|   :view` | ||||
|   """ | ||||
|  | ||||
|   import Phoenix.LiveView.Helpers | ||||
|  | ||||
|   @doc """ | ||||
|   Returns a <time> element that renders the naivedatetime in the user's local | ||||
|   timezone with Alpine.js | ||||
|   """ | ||||
|   @spec display_datetime(NaiveDateTime.t() | nil) :: Phoenix.LiveView.Rendered.t() | ||||
|   def display_datetime(nil), do: "" | ||||
|  | ||||
|   def display_datetime(datetime) do | ||||
|     assigns = %{ | ||||
|       datetime: datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_iso8601(:extended) | ||||
|     } | ||||
|  | ||||
|     ~H""" | ||||
|     <time datetime={@datetime} x-data={"{ | ||||
|         date: | ||||
|           Intl.DateTimeFormat([], {dateStyle: 'short', timeStyle: 'long'}) | ||||
|             .format(new Date(\"#{@datetime}\")) | ||||
|       }"} x-text="date"> | ||||
|       <%= @datetime %> | ||||
|     </time> | ||||
|     """ | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Returns a <date> element that renders the Date in the user's local | ||||
|   timezone with Alpine.js | ||||
|   """ | ||||
|   @spec display_date(Date.t() | nil) :: Phoenix.LiveView.Rendered.t() | ||||
|   def display_date(nil), do: "" | ||||
|  | ||||
|   def display_date(date) do | ||||
|     assigns = %{date: date |> Date.to_iso8601(:extended)} | ||||
|  | ||||
|     ~H""" | ||||
|     <time datetime={@date} x-data={"{ | ||||
|         date: | ||||
|           Intl.DateTimeFormat([], {timeZone: 'Etc/UTC', dateStyle: 'short'}).format(new Date(\"#{@date}\")) | ||||
|       }"} x-text="date"> | ||||
|       <%= @date %> | ||||
|     </time> | ||||
|     """ | ||||
|   end | ||||
| end | ||||
		Reference in New Issue
	
	Block a user