Skip to content

Commit

Permalink
refactor idea cards to live components (#475)
Browse files Browse the repository at this point in the history
  • Loading branch information
JannikStreek authored Nov 12, 2024
1 parent afd6137 commit 4f0acd1
Show file tree
Hide file tree
Showing 12 changed files with 288 additions and 280 deletions.
4 changes: 4 additions & 0 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ Hooks.SetIdeaLabelColor = {
const color = this.el.getAttribute("data-color");
this.el.style.color = color;
},
updated() {
const color = this.el.getAttribute("data-color");
this.el.style.color = color;
}
};

Hooks.SetIdeaLabelBackgroundColor = {
Expand Down
14 changes: 8 additions & 6 deletions lib/mindwendel/idea_labels.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,15 @@ defmodule Mindwendel.IdeaLabels do
end

def remove_idea_label_from_idea(%Idea{} = idea, %IdeaLabel{} = idea_label) do
from(idea_idea_label in IdeaIdeaLabel,
where:
idea_idea_label.idea_id == ^idea.id and
idea_idea_label.idea_label_id == ^idea_label.id
)
|> Repo.delete_all()
result =
from(idea_idea_label in IdeaIdeaLabel,
where:
idea_idea_label.idea_id == ^idea.id and
idea_idea_label.idea_label_id == ^idea_label.id
)
|> Repo.delete_all()

Lanes.broadcast_lanes_update(idea.brainstorming_id)
result
end
end
15 changes: 8 additions & 7 deletions lib/mindwendel/likes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,23 @@ defmodule Mindwendel.Likes do
import Ecto.Query, warn: false
alias Mindwendel.Repo
alias Mindwendel.Ideas
alias Mindwendel.Lanes
alias Mindwendel.Brainstormings
alias Mindwendel.Brainstormings.Like

require Logger

@doc """
Returns a Boolean if a like for the given idea and user exists.
Returns a boolean if like with a given user id exists in the given likes.
This method is primarily used with preloaded data from an idea, therefore it is not needed to reload data from the repo.
## Examples
iex> exists_like_for_idea?(1, 2)
iex> exists_user_in_likes?([...], 2)
true
"""
def exists_like_for_idea?(idea_id, user_id) do
Repo.exists?(from like in Like, where: like.user_id == ^user_id and like.idea_id == ^idea_id)
def exists_user_in_likes?(likes, user_id) do
likes |> Enum.map(fn like -> like.user_id end) |> Enum.member?(user_id)
end

@doc """
Expand All @@ -41,7 +42,7 @@ defmodule Mindwendel.Likes do

case status do
:ok ->
{:ok, Lanes.broadcast_lanes_update(Ideas.get_idea!(idea_id).brainstorming_id)}
{:ok, Brainstormings.broadcast({:ok, Ideas.get_idea!(idea_id)}, :idea_updated)}

:error ->
{:error, result}
Expand All @@ -63,7 +64,7 @@ defmodule Mindwendel.Likes do
from like in Like, where: like.user_id == ^user_id and like.idea_id == ^idea_id
)

Lanes.broadcast_lanes_update(Ideas.get_idea!(idea_id).brainstorming_id)
Brainstormings.broadcast({:ok, Ideas.get_idea!(idea_id)}, :idea_updated)
end

@doc """
Expand Down
10 changes: 5 additions & 5 deletions lib/mindwendel_web/live/brainstorming_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ defmodule MindwendelWeb.BrainstormingLive.Show do
{:noreply, assign(socket, :lanes, new_lanes)}
end

# idea updated - only relevant if the show modal of the idea is opened
def handle_info({:idea_updated, idea}, socket) do
# first, update the specific card of the idea
send_update(MindwendelWeb.IdeaLive.CardComponent, id: idea.id, idea: idea)
# if the idea show modal is opened, also update the idea within the modal
if socket.assigns.live_action == :show_idea and socket.assigns.idea.id == idea.id do
# update the idea modal with new content
send_update(MindwendelWeb.IdeaLive.ShowComponent, id: :show, idea: idea)
{:noreply, socket}
else
{:noreply, socket}
end

{:noreply, socket}
end

def handle_info({:brainstorming_filter_updated, brainstorming, lanes}, socket) do
Expand Down
67 changes: 67 additions & 0 deletions lib/mindwendel_web/live/idea_live/card_component.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
defmodule MindwendelWeb.IdeaLive.CardComponent do
use MindwendelWeb, :live_component
alias Mindwendel.Ideas
alias Mindwendel.IdeaLabels
alias Mindwendel.Likes

@impl true
def handle_event("delete_idea", %{"id" => id}, socket) do
idea = Ideas.get_idea!(id)

%{current_user: current_user, brainstorming: brainstorming} = socket.assigns

if current_user.id in [idea.user_id | brainstorming.moderating_users |> Enum.map(& &1.id)] do
{:ok, _} = Ideas.delete_idea(idea)
end

# broadcast will take care of the removal from the list
{:noreply, socket}
end

def handle_event("like", %{"id" => id}, socket) do
Likes.add_like(id, socket.assigns.current_user.id)

{:noreply, socket}
end

def handle_event("unlike", %{"id" => id}, socket) do
Likes.delete_like(id, socket.assigns.current_user.id)

{:noreply, socket}
end

def handle_event(
"add_idea_label_to_idea",
%{
"idea-id" => idea_id,
"idea-label-id" => idea_label_id
},
socket
) do
idea = Ideas.get_idea!(idea_id)
idea_label = IdeaLabels.get_idea_label(idea_label_id)

case(IdeaLabels.add_idea_label_to_idea(idea, idea_label)) do
{:ok, _idea} ->
{:noreply, socket}

{:error, _changeset} ->
{:noreply, socket}
end
end

def handle_event(
"remove_idea_label_from_idea",
%{
"idea-id" => idea_id,
"idea-label-id" => idea_label_id
},
socket
) do
idea = Ideas.get_idea!(idea_id)
idea_label = IdeaLabels.get_idea_label(idea_label_id)

IdeaLabels.remove_idea_label_from_idea(idea, idea_label)
{:noreply, socket}
end
end
152 changes: 152 additions & 0 deletions lib/mindwendel_web/live/idea_live/card_component.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<div
class={"card-mindwendel m-3 shadow-sm p-2 rounded IndexComponent__IdeaCard #{@width_class}"}
role={if has_move_permission(@brainstorming, @current_user), do: "button", else: ""}
data-testid={@idea.id}
data-id={@idea.id}
data-brainstorming-id={@idea.brainstorming_id}
data-lane-id={@idea.lane_id}
data-position={@idea.position_order}
>
<div class="card-body-mindwendel-idea">
<%= if has_moderating_or_ownership_permission(@brainstorming, @idea, @current_user) do %>
<.link
class="float-end ms-3 mb-3"
phx-click="delete_idea"
phx-target={@myself}
phx-value-id={@idea.id}
title={gettext("Delete idea")}
data-confirm={gettext("Are you sure you want to delete this idea?")}
>
<i class="bi bi-x text-secondary"></i>
</.link>
<.link
patch={~p"/brainstormings/#{@brainstorming.id}/ideas/#{@idea.id}/edit"}
class="float-end ms-3 mb-3"
title={gettext("Edit idea")}
>
<i class="bi bi-pencil text-secondary"></i>
</.link>
<% end %>
<.link
patch={~p"/brainstormings/#{@brainstorming.id}/ideas/#{@idea.id}"}
class="float-end ms-3 mb-3"
title={gettext("Show idea")}
>
<i class="bi bi-eye text-secondary"></i>
</.link>

<%= for idea_label <- Enum.sort_by(@idea.idea_labels, &(&1.position_order)) do %>
<span
id={"idea-label-#{uuid()}"}
class="IndexComponent__IdeaLabelBadge mb-3"
data-testid={idea_label.id}
data-color={idea_label.color}
phx-hook="SetIdeaLabelBackgroundColor"
>
<%= idea_label.name %>
</span>
<% end %>

<%= unless @idea.link do %>
<p class="card-body-mindwendel-idea-text"><%= raw(@idea.body) %></p>
<% end %>

<%= if @idea.link do %>
<.link href={@idea.link.url}>
<%= raw(@idea.body) %>
</.link>
<hr />
<div class="row">
<div class="col-md-3">
<img src={@idea.link.img_preview_url} class="preview-url" />
</div>
<div class="col-md-9">
<p class="fw-bold"><%= @idea.link.title %></p>
<p><%= @idea.link.description %></p>
</div>
</div>
<% end %>

<%= if length(@idea.files) > 0 do %>
<%= for attachment <- @idea.files do %>
<i class="bi bi-file-earmark"></i>
<p class="text-muted">
<.link href={~p"/files/#{attachment.id}"} download={attachment.name}>
<%= attachment.name || gettext("No filename") %>
</.link>
</p>
<% end %>
<% end %>
</div>

<div class="card-footer-mindwendel">
<div class="d-block">
<small class="text-muted">
<%= gettext("By") %> <%= Gettext.gettext(MindwendelWeb.Gettext, @idea.username) %> <%= Timex.format!(
@idea.inserted_at,
"{relative}",
:relative
) %>
</small>
</div>
<div class="d-inline IndexComponent__IdeaLabelSection">
<%= for brainstorming_idea_label <- @brainstorming.labels do %>
<%= unless Enum.find(@idea.idea_labels, fn idea_label -> idea_label.id == brainstorming_idea_label.id end) do %>
<.link
class="text-decoration-none me-1"
data-testid={brainstorming_idea_label.id}
phx-click="add_idea_label_to_idea"
phx-target={@myself}
phx-value-idea-id={@idea.id}
title={"Label #{brainstorming_idea_label.name}"}
phx-value-idea-label-id={brainstorming_idea_label.id}
>
<i
id={"idea-label-#{uuid()}"}
class="IndexComponent__IdeaLabel"
data-testid={brainstorming_idea_label.id}
data-color={brainstorming_idea_label.color}
phx-hook="SetIdeaLabelColor"
>
</i>
</.link>
<% else %>
<.link
class="text-decoration-none me-1"
data-testid={brainstorming_idea_label.id}
phx-click="remove_idea_label_from_idea"
phx-target={@myself}
phx-value-idea-id={@idea.id}
title={"Label #{brainstorming_idea_label.name}"}
phx-value-idea-label-id={brainstorming_idea_label.id}
>
<i
id={"idea-label-#{uuid()}"}
class="IndexComponent__IdeaLabel--active"
data-testid={brainstorming_idea_label.id}
data-color={brainstorming_idea_label.color}
phx-hook="SetIdeaLabelColor"
>
</i>
</.link>
<% end %>
<% end %>
</div>
<div class="float-end ms-2">
<span>
<%= @idea.comments_count %>
<i class="bi-chat"></i>
</span>
<span class="me-1"><%= length(@idea.likes) %></span>
<%= if Mindwendel.Likes.exists_user_in_likes?(@idea.likes, @current_user.id) do %>
<.link phx-click="unlike" phx-target={@myself} phx-value-id={@idea.id} title="Unlike">
<i class="bi-arrow-up-circle-fill"></i>
</.link>
<% else %>
<.link phx-click="like" phx-target={@myself} phx-value-id={@idea.id} title="Like">
<i class="bi-arrow-up-circle"></i>
</.link>
<% end %>
</div>
</div>
</div>
Loading

0 comments on commit 4f0acd1

Please sign in to comment.