Skip to content

Commit

Permalink
feat: members tab
Browse files Browse the repository at this point in the history
  • Loading branch information
ruilopesm committed Sep 11, 2024
1 parent b31e981 commit 54c627e
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 44 deletions.
39 changes: 18 additions & 21 deletions lib/atomic/organizations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,25 @@ defmodule Atomic.Organizations do
end

@doc """
Returns the list of organizations where an user is an admin or owner.
Returns the list of organizations which are connected with the user.
By default, it returns the organizations where the user is an admin or owner.
## Examples
iex> list_user_organizations(user_id)
iex> list_user_organizations(123)
[%Organization{}, ...]
iex> list_user_organizations(456)
[]
iex> list_user_organizations(123, [:follower])
[%Organization{}, ...]
"""
def list_user_organizations(user_id, opts \\ []) do
def list_user_organizations(user_id, roles \\ [:admin, :owner], opts \\ []) do
Organization
|> join(:inner, [o], m in Membership, on: m.organization_id == o.id)
|> where([o, m], m.user_id == ^user_id and m.role in [:admin, :owner])
|> where([o, m], m.user_id == ^user_id and m.role in ^roles)
|> apply_filters(opts)
|> Repo.all()
end
Expand Down Expand Up @@ -256,30 +264,19 @@ defmodule Atomic.Organizations do
end

@doc """
Returns the list of memberships.
Returns the list of members in an organization.
A member is someone who is connected to the organization with a role other than `:follower`.
## Examples
iex> list_memberships(%{"organization_id" => id})
[%Organization{}, ...]
iex> list_memberships(%{"user_id" => id})
iex> list_memberships(123)
[%Organization{}, ...]
"""
def list_memberships(params, preloads \\ [])

def list_memberships(%{"organization_id" => organization_id}, preloads) do
def list_memberships(organization_id, opts \\ []) do
Membership
|> where([a], a.organization_id == ^organization_id and a.role != :follower)
|> Repo.all()
|> Repo.preload(preloads)
end

def list_memberships(%{"user_id" => user_id}, preloads) do
Membership
|> where([a], a.user_id == ^user_id)
|> Repo.preload(preloads)
|> where([m], m.organization_id == ^organization_id and m.role != :follower)
|> apply_filters(opts)
|> Repo.all()
end

Expand Down
4 changes: 2 additions & 2 deletions lib/atomic_web/live/home_live/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ defmodule AtomicWeb.HomeLive.Index do
current_user = socket.assigns.current_user

%{entries: entries, metadata: metadata} =
Organizations.list_memberships(%{"user_id" => current_user.id})
|> Enum.map(& &1.organization_id)
Organizations.list_user_organizations(current_user.id, [:follower])
|> Enum.map(& &1.id)
|> Feed.list_posts_following_paginated([])

{:noreply,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule AtomicWeb.OrganizationLive.Components.OrganizationAbout do
defmodule AtomicWeb.OrganizationLive.Components.About do
@moduledoc false
use AtomicWeb, :component

Expand All @@ -7,7 +7,7 @@ defmodule AtomicWeb.OrganizationLive.Components.OrganizationAbout do

attr :organization, Organization, required: true, doc: "the organization which about to display"

def organization_about(assigns) do
def about(assigns) do
~H"""
<div id="organization-about">
<h2 class="mb-2 flex-1 select-none truncate text-lg font-semibold text-zinc-900"><%= gettext("Description") %></h2>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule AtomicWeb.OrganizationLive.Components.OrganizationDepartments do
defmodule AtomicWeb.OrganizationLive.Components.DepartmentsGrid do
@moduledoc """
Internal organization-related component for displaying its departments.
"""
Expand All @@ -14,7 +14,7 @@ defmodule AtomicWeb.OrganizationLive.Components.OrganizationDepartments do
required: true,
doc: "the organization which departments to display"

def organization_departments(assigns) do
def departments_grid(assigns) do
~H"""
<div id="organization-departments" class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-2">
<%= for department <- list_departments(@organization) do %>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
defmodule AtomicWeb.OrganizationLive.Components.MembershipBanner do
@moduledoc """
Organization membership banner component. Displays information about the organization's membership benefits and price.
"""
use AtomicWeb, :component

alias Atomic.Organizations.Organization

attr :organization, Organization,
required: true,
doc: "the organization which membership banner to display"

def membership_banner(assigns) do
~H"""
<div class="mx-auto rounded-3xl ring-1 ring-gray-200 lg:mx-0 lg:flex lg:max-w-none">
<div class="p-8 sm:p-10">
<h3 class="text-2xl font-bold tracking-tight text-gray-900"><%= gettext("Lifetime membership") %></h3>
<div class="mt-10 flex items-center gap-x-4">
<h4 class="flex-none text-sm font-semibold leading-6 text-orange-600"><%= gettext("What’s included") %></h4>
<div class="h-px flex-auto bg-gray-100"></div>
</div>
<ul role="list" class="mt-8 grid grid-cols-1 gap-4 text-sm leading-6 text-gray-600 sm:grid-cols-2 sm:gap-6">
<li class="flex gap-x-3">
<.icon name={:check} class="h-6 w-5 flex-none text-orange-600" />
<p>Access to our room facilities</p>
</li>
<li class="flex gap-x-3">
<.icon name={:check} class="h-6 w-5 flex-none text-orange-600" />
<p>Free access to all activities</p>
</li>
<li class="flex gap-x-3">
<.icon name={:check} class="h-6 w-5 flex-none text-orange-600" />
<p>Official member t-shirt</p>
</li>
</ul>
</div>
<div class="-mt-2 p-2 lg:mt-0 lg:w-full lg:max-w-md lg:flex-shrink-0">
<div class="ring-gray-900/5 rounded-2xl bg-gray-50 py-10 text-center ring-1 ring-inset lg:flex lg:flex-col lg:justify-center lg:py-16">
<div class="mx-auto flex max-w-xs flex-col items-center px-8">
<p class="text-base font-semibold text-gray-600"><%= gettext("Pay once, be a member forever") %></p>
<p class="mt-6 flex items-baseline justify-center gap-x-2">
<span class="text-5xl font-bold tracking-tight text-gray-900">10€</span>
<span class="text-sm font-semibold leading-6 tracking-wide text-gray-600">EUR</span>
</p>
<.button icon={:banknotes} class="mt-10 text-sm"><%= gettext("Request your membership") %></.button>
<p class="mt-6 text-xs leading-5 text-gray-600"><%= gettext("Payments should be made within our location.") %> <%= @organization.location %></p>
</div>
</div>
</div>
</div>
"""
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule AtomicWeb.OrganizationLive.Components.MembershipsTable do
@moduledoc """
Internal organization-related component for displaying its memberships in the form of a table.
"""
use AtomicWeb, :component

import AtomicWeb.Components.Avatar

attr :members, :list, required: true, doc: "the list of memberships to display"

# TODO: Make use of table component
def memberships_table(assigns) do
~H"""
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<table class="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th scope="col" class="py-3 pr-3 pl-4 text-left text-xs font-medium uppercase tracking-wide text-gray-500 sm:pl-0"><%= gettext("Name") %></th>
<th scope="col" class="px-3 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500"><%= gettext("Role") %></th>
<th scope="col" class="px-3 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500"><%= gettext("Joined At") %></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
<tr :for={member <- @members} class="hover:cursor-pointer hover:bg-gray-50" phx-click={row_click(member)}>
<td class="whitespace-nowrap py-5 pr-3 pl-4 text-sm sm:pl-0">
<div class="flex items-center">
<.avatar name={member.user.name} size={:sm} color={:light_gray} class="ring-1 ring-white" />
<div class="ml-4">
<div class="font-medium text-gray-900"><%= member.user.name %></div>
<div class="mt-1 text-gray-500"><%= member.user.email %></div>
</div>
</div>
</td>
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900"><%= capitalize_first_letter(member.role) %></td>
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500"><%= relative_datetime(member.inserted_at) %></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
"""
end

defp row_click(member) do
Routes.profile_show_path(AtomicWeb.Endpoint, :show, member.user)
|> JS.navigate()
end
end
15 changes: 14 additions & 1 deletion lib/atomic_web/live/organization_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ defmodule AtomicWeb.OrganizationLive.Show do
alias Atomic.{Accounts, Organizations}

import AtomicWeb.Components.{Gradient, Tabs}
import AtomicWeb.OrganizationLive.Components.{OrganizationAbout, OrganizationDepartments}

import AtomicWeb.OrganizationLive.Components.{
About,
DepartmentsGrid,
MembershipsTable,
MembershipBanner
}

@impl true
def mount(_params, _session, socket) do
Expand All @@ -14,16 +20,23 @@ defmodule AtomicWeb.OrganizationLive.Show do
@impl true
def handle_params(%{"id" => id} = params, _, socket) do
organization = Organizations.get_organization!(id)
members = maybe_list_members(organization.id, params["tab"])

{:noreply,
socket
|> assign(:page_title, organization.name)
|> assign(:current_page, :organization)
|> assign(:current_tab, current_tab(socket, params))
|> assign(:organization, organization)
|> assign(:members, members)
|> assign(:has_permissions?, has_permissions?(socket))}
end

defp maybe_list_members(organization_id, "members"),
do: Organizations.list_memberships(organization_id, preloads: [:user])

defp maybe_list_members(_organization, _tab), do: nil

defp current_tab(_socket, params) when is_map_key(params, "tab"), do: params["tab"]
defp current_tab(_socket, _params), do: "about"

Expand Down
21 changes: 16 additions & 5 deletions lib/atomic_web/live/organization_live/show.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,24 @@
</.link>
</.tabs>
<div class="mt-8 px-4 sm:px-6 lg:px-8">
<div :if={@current_tab == "about"}>
<.organization_about organization={@organization} />
<div class="mt-8">
<div :if={@current_tab == "about"} class="px-4 sm:px-6 lg:px-8">
<.about organization={@organization} />
</div>
<div :if={@current_tab == "departments"}>
<.organization_departments organization={@organization} />
<div :if={@current_tab == "departments"} class="px-4 sm:px-6 lg:px-8">
<.departments_grid organization={@organization} />
</div>
<div :if={@current_tab == "members"} class="px-4 sm:px-6 lg:px-8">
<.membership_banner organization={@organization} />
</div>
<!-- Because divider can't have padding, we need to separate it from the rest of the content -->
<div :if={@current_tab == "members"} class="my-8 w-full min-h-full border-b" />
<div :if={@current_tab == "members"} class="px-4 sm:px-6 lg:px-8">
<!-- Members are directly passed since their calculation depends on the current tab -->
<.memberships_table members={@members} />
</div>
</div>
</.page>
12 changes: 1 addition & 11 deletions test/atomic/organizations_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,7 @@ defmodule Atomic.OrganizationsTest do
membership = insert(:membership, role: :admin)

memberships =
Organizations.list_memberships(%{"organization_id" => membership.organization_id})
|> Enum.map(& &1.id)

assert memberships == [membership.id]
end

test "list_memberships/1 returns all memberships of user" do
membership = insert(:membership)

memberships =
Organizations.list_memberships(%{"user_id" => membership.user_id})
Organizations.list_memberships(membership.organization_id)
|> Enum.map(& &1.id)

assert memberships == [membership.id]
Expand Down

0 comments on commit 54c627e

Please sign in to comment.