-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #91 from briankariuki/feature/tags
add user interests, closes #67
- Loading branch information
Showing
11 changed files
with
615 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.