rename to memex

This commit is contained in:
2022-07-25 19:31:54 -04:00
parent 65ec4286da
commit 1a423f703b
122 changed files with 416 additions and 416 deletions

View 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

View 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>

View 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

View 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

View 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>

View 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

View 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 %>

View 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