Skip to content

Commit

Permalink
Merge pull request #91 from briankariuki/feature/tags
Browse files Browse the repository at this point in the history
add user interests, closes #67
  • Loading branch information
wintermeyer authored Feb 29, 2024
2 parents 55585e0 + 73f2719 commit cedce42
Show file tree
Hide file tree
Showing 11 changed files with 615 additions and 1 deletion.
5 changes: 5 additions & 0 deletions lib/animina/accounts/resources/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Animina.Accounts.User do
extensions: [AshAuthentication]

alias Animina.Accounts
alias Animina.Traits
alias Animina.Validations

attributes do
Expand Down Expand Up @@ -73,6 +74,10 @@ defmodule Animina.Accounts.User do
relationships do
has_many :credits, Accounts.Credit
has_many :photos, Accounts.Photo

has_many :interests, Traits.UserInterests do
api Traits
end
end

validations do
Expand Down
59 changes: 59 additions & 0 deletions lib/animina/traits/resources/user_interests.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
defmodule Animina.Traits.UserInterests do
@moduledoc """
This is the UserInterests module which we use to manage a user's interests.
"""

use Ash.Resource,
data_layer: AshPostgres.DataLayer

attributes do
uuid_primary_key :id

attribute :flag_id, :uuid, allow_nil?: false
attribute :user_id, :uuid, allow_nil?: false
end

relationships do
belongs_to :user, Animina.Accounts.User do
api Animina.Accounts
allow_nil? false
attribute_writable? true
end

has_one :flag, Animina.Traits.Flag do
source_attribute :flag_id
destination_attribute :id
end
end

actions do
defaults [:create, :read]
end

code_interface do
define_for Animina.Traits
define :read
define :create
define :by_id, get_by: [:id], action: :read
end

preparations do
prepare build(load: [:flag, :user_id])
end

postgres do
table "user_interests"
repo Animina.Repo

references do
reference :user, on_delete: :delete
reference :flag, on_delete: :delete
end

custom_indexes do
index [:user_id]
index [:flag_id]
index [:flag_id, :user_id]
end
end
end
1 change: 1 addition & 0 deletions lib/animina/traits/traits.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ defmodule Animina.Traits do
resource Animina.Traits.CategoryTranslation
resource Animina.Traits.Flag
resource Animina.Traits.FlagTranslation
resource Animina.Traits.UserInterests
end
end
112 changes: 112 additions & 0 deletions lib/animina_web/components/select_flags_component.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
defmodule AniminaWeb.SelectFlagsComponent do
@moduledoc """
This component renders flags which a user can click on.
"""
use AniminaWeb, :live_component

@impl true
def mount(socket) do
{:ok, socket |> assign(selected_flags: %{})}
end

@impl true
def update(assigns, socket) do
socket =
socket
|> assign(assigns)

{:ok, socket}
end

@impl true
def handle_event(
"select_flag",
%{
"category" => category,
"categoryid" => category_id,
"flag" => flag,
"flagid" => flag_id
},
socket
) do
socket =
case Map.get(socket.assigns.selected_flags, flag_id) do
nil ->
send(self(), {:flag_selected, flag_id})

socket
|> assign(
:selected_flags,
Map.merge(socket.assigns.selected_flags, %{
"#{flag_id}" => %{
"category" => %{
"id" => category_id,
"name" => category
},
"flag" => %{
"id" => flag_id,
"name" => flag
}
}
})
)

_ ->
send(self(), {:flag_unselected, flag_id})

socket
|> assign(
:selected_flags,
Map.drop(socket.assigns.selected_flags, [flag_id])
)
end

{:noreply, socket}
end

@impl true
def render(assigns) do
~H"""
<div class="py-4 space-y-2">
<h3 class="font-semibold text-gray-800 truncate">
<%= get_translation(@category.category_translations, @language) %>
</h3>
<ol class="flex flex-wrap gap-2 w-full">
<li :for={flag <- @category.flags}>
<div
phx-value-category={@category.name}
phx-value-categoryid={@category.id}
phx-value-flag={flag.name}
phx-value-flagid={flag.id}
phx-target={@myself}
aria-label="button"
phx-click={
if(@can_select || Map.get(@selected_flags, flag.id) != nil,
do: "select_flag",
else: nil
)
}
class={
if(@can_select || (@can_select == false && Map.get(@selected_flags, flag.id) != nil), do: "cursor-pointer ", else: "cursor-not-allowed ") <>
"rounded-full px-3 py-1.5 text-sm font-semibold leading-6 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
<> if(Map.get(@selected_flags, flag.id) != nil, do: "hover:bg-indigo-500 bg-indigo-600 focus-visible:outline-indigo-600 text-white shadow-sm", else: "hover:bg-indigo-50 bg-indigo-100 focus-visible:outline-indigo-100 text-indigo-600 shadow-none")
}
>
<%= get_translation(flag.flag_translations, @language) %>
</div>
</li>
</ol>
</div>
"""
end

defp get_translation(translations, language) do
language = String.split(language, "-") |> Enum.at(0)

translation =
Enum.find(translations, nil, fn translation -> translation.language == language end)

translation.name
end
end
164 changes: 164 additions & 0 deletions lib/animina_web/live/interests_live.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
defmodule AniminaWeb.InterestsLive do
use AniminaWeb, :live_view

alias Animina.Traits
alias AniminaWeb.Registration
alias AniminaWeb.SelectFlagsComponent
alias Phoenix.LiveView.AsyncResult

@impl true
def mount(_params, %{"language" => language} = session, socket) do
current_user =
case Registration.get_current_user(session) do
nil ->
redirect(socket, to: "/")

user ->
user
end

socket =
socket
|> assign(current_user: current_user)
|> assign(max_selected: 20)
|> assign(selected: 0)
|> assign(active_tab: :home)
|> assign(selected_flags: [])
|> assign(language: language)
|> assign(page_title: gettext("Select your interests"))
|> assign(categories: AsyncResult.loading())
|> stream(:categories, [])
|> start_async(:fetch_categories, fn -> fetch_categories() end)

{:ok, socket}
end

@impl true
def handle_async(:fetch_categories, {:ok, fetched_categories}, socket) do
%{categories: categories} = socket.assigns

{:noreply,
socket
|> assign(
:categories,
AsyncResult.ok(categories, Enum.map(fetched_categories, fn category -> category.id end))
)
|> stream(:categories, fetched_categories)}
end

@impl true
def handle_async(:fetch_categories, {:exit, reason}, socket) do
%{categories: categories} = socket.assigns
{:noreply, assign(socket, :categories, AsyncResult.failed(categories, {:exit, reason}))}
end

@impl true
def handle_info({:flag_selected, flag_id}, socket) do
selected = socket.assigns.selected + 1
can_select = selected < socket.assigns.max_selected

for category_id <- socket.assigns.categories.result do
send_update(SelectFlagsComponent, id: "flags_#{category_id}", can_select: can_select)
end

{:noreply,
socket
|> assign(:selected_flags, List.insert_at(socket.assigns.selected_flags, -1, flag_id))
|> assign(:selected, selected)}
end

@impl true
def handle_info({:flag_unselected, flag_id}, socket) do
selected = max(socket.assigns.selected - 1, 0)
can_select = selected < socket.assigns.max_selected

for category_id <- socket.assigns.categories.result do
send_update(SelectFlagsComponent, id: "flags_#{category_id}", can_select: can_select)
end

{:noreply,
socket
|> assign(:selected_flags, List.delete(socket.assigns.selected_flags, flag_id))
|> assign(:selected, selected)}
end

@impl true
def handle_event("add_interests", _params, socket) do
interests =
Enum.map(socket.assigns.selected_flags, fn flag_id ->
%{
flag_id: flag_id,
user_id: socket.assigns.current_user.id
}
end)

bulk_result =
Traits.bulk_create(interests, Traits.UserInterests, :create, stop_on_error?: true)

case bulk_result.status do
:error ->
{:noreply,
socket |> put_flash(:error, gettext("Something went wrong adding your interests"))}

_ ->
{:noreply,
socket
|> assign(selected: 0)
|> assign(selected_flags: [])
|> put_flash(:info, gettext("Your interests have been added succesfully"))
|> push_navigate(to: "/registration/interests")}
end
end

defp fetch_categories do
Traits.Category
|> Ash.Query.for_read(:read)
|> Traits.read!()
end

@impl true
def render(assigns) do
~H"""
<div class="space-y-8 px-5">
<.notification_box
title={gettext("Hello %{name}!", name: @current_user.name)}
message={gettext("To complete your profile choose topics you are interested in")}
/>
<h2 class="font-bold text-xl"><%= gettext("Choose your interests") %></h2>
<.async_result :let={_categories} assign={@categories}>
<:loading><%= gettext("Loading interests...") %></:loading>
<:failed :let={_failure}><%= gettext("There was an error loading interests") %></:failed>
<div id="stream_categories" phx-update="stream">
<div :for={{dom_id, category} <- @streams.categories} id={"#{dom_id}"}>
<.live_component
module={SelectFlagsComponent}
id={"flags_#{category.id}"}
category={category}
language={@language}
can_select={@selected < @max_selected}
/>
</div>
</div>
<div class="pb-8">
<button
phx-click="add_interests"
class={
"flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 " <>
unless(@selected == 0,
do: "",
else: "opacity-40 cursor-not-allowed hover:bg-blue-500 active:bg-blue-500"
)}
disabled={@selected == 0}
>
<%= gettext("Add interests") %>
</button>
</div>
</.async_result>
</div>
"""
end
end
3 changes: 2 additions & 1 deletion lib/animina_web/live/profile_photo_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ defmodule AniminaWeb.ProfilePhotoLive do
|> assign(
:form,
Form.for_create(Photo, :create, api: Accounts, as: "photo")
)}
)
|> push_navigate(to: ~p"/registration/interests")}
else
{:error, form} ->
{:noreply, socket |> assign(:form, form)}
Expand Down
1 change: 1 addition & 0 deletions lib/animina_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ defmodule AniminaWeb.Router do
live "/", RootLive, :register
live "/registration/potential-partner", PotentialPartnerLive, :index
live "/registration/profile-photo", ProfilePhotoLive, :index
live "/registration/interests", InterestsLive, :index
# live "/register", AniminaWeb.AuthLive.Index, :register
live "/sign-in", AniminaWeb.AuthLive.Index, :sign_in

Expand Down
Loading

0 comments on commit cedce42

Please sign in to comment.