Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve sidebar #572

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/atomic/organizations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ defmodule Atomic.Organizations do
[%Organization{}, ...]

"""
def list_organizations(params \\ %{})
def list_organizations do
Organization |> Repo.all()
end

def list_organizations(opts) when is_list(opts) do
Organization
Expand Down
86 changes: 86 additions & 0 deletions lib/atomic_web/components/accordion.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
defmodule AtomicWeb.Components.Accordion do
@moduledoc """
Provides accordion-related components and helper functions.
"""
use AtomicWeb, :component

alias Phoenix.LiveView.JS
import AtomicWeb.Components.Icon

@doc """
Accordion components allows users to show and hide sections of related panel on a page.

## Examples

```heex
<.accordion>
<:trigger>Accordion</:trigger>
<:panel>Content</:panel>
</.accordion>
```
"""

attr :class, :any, doc: "Extend existing component styles"
attr :controlled, :boolean, default: false
attr :id, :string, required: true
attr :rest, :global

slot :trigger, validate_attrs: false
slot :panel, validate_attrs: false

@spec accordion(Socket.assigns()) :: Rendered.t()
def accordion(assigns) do
~H"""
<div class={["accordion", assigns[:class]]} id={@id} {@rest}>
<%= for {{trigger, panel}, idx} <- @trigger |> Enum.zip(@panel) |> Enum.with_index() do %>
<h3>
<button
aria-controls={panel_id(@id, idx)}
aria-expanded={to_string(panel[:default_expanded] == true)}
class={[
"accordion-trigger relative w-full [&_.accordion-trigger-icon]:aria-expanded:rotate-180",
trigger[:class]
]}
id={trigger_id(@id, idx)}
phx-click={handle_click(assigns, idx)}
type="button"
{assigns_to_attributes(trigger, [:class, :icon_name])}
>
{render_slot(trigger)}
<.icon class="accordion-trigger-icon absolute top-1/2 right-4 h-5 w-5 -translate-y-1/2 transition-all duration-300 ease-in-out" name={trigger[:icon_name] || "hero-chevron-down"} />
</button>
</h3>
<div class="accordion-panel grid-rows-[0fr] grid transform transition-all duration-200 ease-in data-[expanded]:grid-rows-[1fr]" data-expanded={panel[:default_expanded]} id={panel_id(@id, idx)} role="region">
<div class="overflow-hidden">
<div class={["accordion-panel-content", panel[:class]]} {assigns_to_attributes(panel, [:class, :default_expanded ])}>
{render_slot(panel)}
</div>
</div>
</div>
<% end %>
</div>
"""
end

defp trigger_id(id, idx), do: "#{id}_trigger#{idx}"
defp panel_id(id, idx), do: "#{id}_panel#{idx}"

defp handle_click(%{controlled: controlled, id: id}, idx) do
op =
{"aria-expanded", "true", "false"}
|> JS.toggle_attribute(to: "##{trigger_id(id, idx)}")
|> JS.toggle_attribute({"data-expanded", ""}, to: "##{panel_id(id, idx)}")

if controlled do
op
|> JS.set_attribute({"aria-expanded", "false"},
to: "##{id} .accordion-trigger:not(##{trigger_id(id, idx)})"
)
|> JS.remove_attribute("data-expanded",
to: "##{id} .accordion-panel:not(##{panel_id(id, idx)})"
)
else
op
end
end
end
74 changes: 50 additions & 24 deletions lib/atomic_web/components/organizations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,69 @@ defmodule AtomicWeb.Components.Organizations do
@moduledoc false
use AtomicWeb, :live_component

import AtomicWeb.Components.Avatar
import AtomicWeb.Components.{Avatar, Accordion}

alias Atomic.Accounts
alias Atomic.Organizations

@impl true
def render(assigns) do
~H"""
<ul role="list" class="-mx-2 mt-2 max-h-72 max-h-72 space-y-1 overflow-y-auto">
<%= for organization <- @organizations do %>
<li>
<div
phx-target={@myself}
phx-click="select-organization"
phx-value-organization_id={organization.id}
class={
<div id={@id}>
<.accordion id={"#{@id}-accordion"} class="flex-grow rounded-md border" controlled={true}>
<:trigger>
<%= if @current_organization do %>
<div class="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-zinc-700 hover:text-primary-500">
<.avatar
class={"#{if @current_organization && @current_organization.id == @current_organization.id do "border-primary-600" else "border-zinc-200" end} #{(@current_organization && @current_organization.id == @current_organization.id) && "text-primary-600"} border group-hover:border-primary-600 group-hover:text-primary-500"}
src={Uploaders.Logo.url({@current_organization.logo, @current_organization}, :original)}
name={@current_organization.name}
size={:xs}
type={:organization}
color={:white}
/>
<span class="mt-1 truncate">{@current_organization.name}</span>
</div>
<% else %>
<div class="group cursor-pointer gap-x-3 rounded-md p-2 text-left text-sm leading-6 text-zinc-600 hover:text-primary-500">
<.icon name="hero-pencil-solid" class="size-5 shrink-0 text-zinc-400 group-hover:text-primary-500" />
<span class="mt-1 truncate">{gettext("Pick an organization")}</span>
</div>
<% end %>
</:trigger>
<:panel>
<ul role="list" class="mt-2 max-h-72 space-y-0.5 overflow-y-auto overscroll-contain p-1">
<%= for organization <- @organizations do %>
<li>
<div
phx-target={@myself}
phx-click="select-organization"
phx-value-organization_id={organization.id}
class={
"#{if @current_organization && organization.id == @current_organization.id do
"bg-zinc-50 text-primary-500"
else
"text-zinc-700 hover:text-primary-500 hover:bg-zinc-50"
end} group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold cursor-pointer"
}
type="button"
>
<.avatar
class={"#{if @current_organization && organization.id == @current_organization.id do "border-primary-600" else "border-zinc-200" end} #{(@current_organization && organization.id == @current_organization.id) && "text-primary-600"} border group-hover:border-primary-600 group-hover:text-primary-500"}
src={Uploaders.Logo.url({organization.logo, organization}, :original)}
name={organization.name}
size={:xs}
type={:organization}
color={:white}
/>
<span class="mt-1 truncate">{organization.name}</span>
</div>
</li>
<% end %>
</ul>
type="button"
>
<.avatar
class={"#{if @current_organization && organization.id == @current_organization.id do "border-primary-600" else "border-zinc-200" end} #{(@current_organization && organization.id == @current_organization.id) && "text-primary-600"} border group-hover:border-primary-600 group-hover:text-primary-500"}
src={Uploaders.Logo.url({organization.logo, organization}, :original)}
name={organization.name}
size={:xs}
type={:organization}
color={:white}
/>
<span class="mt-1 truncate">{organization.name}</span>
</div>
</li>
<% end %>
</ul>
</:panel>
</.accordion>
</div>
"""
end

Expand Down
29 changes: 21 additions & 8 deletions lib/atomic_web/components/sidebar.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ defmodule AtomicWeb.Components.Sidebar do
<.sidebar_dropdown current_user={@current_user} orientation={:down} />
</div>
</div>
<div id="sidebar-overlay" class="fixed inset-0 z-40 hidden cursor-pointer bg-black bg-opacity-50" phx-click={hide_mobile_sidebar()}></div>
<div id="sidebar-overlay" class="fixed inset-0 z-40 hidden cursor-pointer bg-black bg-opacity-50 backdrop-blur-sm" phx-click={hide_mobile_sidebar()}></div>
<!-- Sidebar Panel -->
<div id="mobile-sidebar" class="fixed inset-0 z-50 hidden w-64" role="dialog" aria-modal="true">
<div class="fixed inset-0 flex w-fit">
<div class="relative flex w-64 max-w-xs flex-col border-r bg-white">
<div class="relative flex w-72 max-w-xs flex-col rounded-r-md border-r bg-white">
<div class="flex justify-between p-4">
<.sidebar_header />

Expand Down Expand Up @@ -103,14 +103,17 @@ defmodule AtomicWeb.Components.Sidebar do
<ul role="list" class="-mx-2 space-y-1">
<%= for page <- AtomicWeb.Config.pages(@current_user, @current_organization) do %>
<li class="select-none">
<.link navigate={page.url} class={"#{if @current_page == page.key do "bg-zinc-50 text-primary-500" else "text-zinc-700 hover:text-primary-500 hover:bg-zinc-50" end} group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6"}>
<.icon name={page.icon} class={
<.link navigate={page.url} class={"#{if @current_page == page.key do "font-extrabold text-primary-500" else "text-zinc-700 hover:text-primary-500 hover:bg-zinc-50 font-medium" end} group flex gap-x-3 rounded-md p-2 leading-6"}>
<.icon
name={if @current_page == page.key, do: page.icon_selected, else: page.icon}
class={
"#{if @current_page == page.key do
"text-primary-500"
else
"text-zinc-400 group-hover:text-primary-500"
end} size-6 shrink-0"
} />
end} size-7 shrink-0"
}
/>
{page.title}
</.link>
</li>
Expand Down Expand Up @@ -173,7 +176,10 @@ defmodule AtomicWeb.Components.Sidebar do
transition:
{"transition ease-in-out duration-300 transform", "-translate-x-full", "translate-x-0"}
)
|> JS.show(to: "#sidebar-overlay")
|> JS.show(
to: "#sidebar-overlay",
transition: {"ease-in duration-300", "opacity-0", "opacity-100"}
)
|> JS.dispatch("focus", to: "#mobile-sidebar")
end

Expand All @@ -200,5 +206,12 @@ defmodule AtomicWeb.Components.Sidebar do
end

defp get_organizations(nil), do: []
defp get_organizations(user), do: Organizations.list_user_organizations(user.id)

defp get_organizations(user) do
if user.role == :master do
Organizations.list_organizations()
else
Organizations.list_user_organizations(user.id)
end
end
end
9 changes: 9 additions & 0 deletions lib/atomic_web/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ defmodule AtomicWeb.Config do
key: :scanner,
title: "Scanner",
icon: "hero-qr-code",
icon_selected: "hero-qr-code-solid",
url: ~p"/scanner",
tabs: []
}
Expand All @@ -40,27 +41,31 @@ defmodule AtomicWeb.Config do
key: :departments,
title: "Departments",
icon: "hero-cube",
icon_selected: "hero-cube-solid",
url: ~p"/organizations/#{current_organization}/departments",
tabs: []
},
%{
key: :announcements,
title: "Announcements",
icon: "hero-newspaper",
icon_selected: "hero-newspaper-solid",
url: ~p"/organizations/#{current_organization}/announcements",
tabs: []
},
%{
key: :partners,
title: "Partners",
icon: "hero-user-group",
icon_selected: "hero-user-group-solid",
url: ~p"/organizations/#{current_organization}/partners",
tabs: []
},
%{
key: :scanner,
title: "Scanner",
icon: "hero-qr-code",
icon_selected: "hero-qr-code-solid",
url: ~p"/scanner",
tabs: []
}
Expand All @@ -73,27 +78,31 @@ defmodule AtomicWeb.Config do
key: :home,
title: "Home",
icon: "hero-home",
icon_selected: "hero-home-solid",
url: ~p"/",
tabs: []
},
%{
key: :calendar,
title: "Calendar",
icon: "hero-calendar",
icon_selected: "hero-calendar-solid",
url: ~p"/calendar",
tabs: []
},
%{
key: :activities,
title: "Activities",
icon: "hero-academic-cap",
icon_selected: "hero-academic-cap-solid",
url: ~p"/activities",
tabs: []
},
%{
key: :organizations,
title: "Organizations",
icon: "tabler-affiliate",
icon_selected: "tabler-affiliate-filled",
url: ~p"/organizations",
tabs: []
}
Expand Down
Loading