Skip to content

Commit

Permalink
feat: multiselect component & activity speakers
Browse files Browse the repository at this point in the history
  • Loading branch information
joaodiaslobo committed Nov 4, 2024
1 parent 487ec89 commit a5e5417
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 24 deletions.
26 changes: 26 additions & 0 deletions assets/css/components/field.css
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,29 @@
.safira-form-help-text {
@apply mt-2 text-sm text-gray-500 dark:text-gray-400;
}

/* Multiselect */

.safira-multiselect-dropdown {
@apply absolute z-50 border border-t-0 border-lightShade dark:border-darkShade mt-1 rounded-b-md pb-1 bg-light dark:bg-dark;
}

.safira-multiselect-dropdown-option {
@apply mx-1 my-1 px-3 py-1 hover:bg-lightShade/40 dark:hover:bg-darkShade cursor-pointer rounded-md text-sm;
}

.safira-multiselect-dropdown-option-selected {
@apply opacity-60;
}

.safira-multiselect-dropdown-tags-container {
@apply absolute flex mt-12 gap-1 flex-wrap;
}

.safira-multiselect-dropdown-tag {
@apply text-xs flex items-center border border-lightShade dark:border-darkShade pl-2 rounded-md z-30;
}

.safira-multiselect-dropdown-tag-remove {
@apply cursor-pointer dark:opacity-80 scale-75;
}
4 changes: 3 additions & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ import "phoenix_html"
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
import live_select from "live_select"
import { QrScanner, Wheel, Confetti, Sorting } from "./hooks";

let Hooks = {
QrScanner: QrScanner,
Wheel: Wheel,
Confetti: Confetti,
Sorting: Sorting
Sorting: Sorting,
...live_select
};

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
Expand Down
40 changes: 35 additions & 5 deletions lib/safira/activities.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule Safira.Activities do

use Safira.Context

alias Safira.Activities.{Activity, Speaker}
alias Safira.Activities.{Activity, ActivityCategory, Speaker}

@doc """
Returns the list of activities.
Expand All @@ -17,22 +17,27 @@ defmodule Safira.Activities do
"""
def list_activities do
Repo.all(Activity)
Activity
|> preload(:speakers)
|> Repo.all()
end

def list_activities(opts) when is_list(opts) do
Activity
|> apply_filters(opts)
|> preload(:speakers)
|> Repo.all()
end

def list_activities(params) do
Activity
|> preload(:speakers)
|> Flop.validate_and_run(params, for: Activity)
end

def list_activities(%{} = params, opts) when is_list(opts) do
Activity
|> preload(:speakers)
|> apply_filters(opts)
|> Flop.validate_and_run(params, for: Activity)
end
Expand All @@ -51,7 +56,11 @@ defmodule Safira.Activities do
** (Ecto.NoResultsError)
"""
def get_activity!(id), do: Repo.get!(Activity, id)
def get_activity!(id) do
Activity
|> preload(:speakers)
|> Repo.get!(id)
end

@doc """
Creates a activity.
Expand Down Expand Up @@ -89,6 +98,29 @@ defmodule Safira.Activities do
|> Repo.update()
end

@doc """
Updates an activity's speakers.
## Examples
iex> upsert_activity_speakers(activity, [1, 2, 3])
{:ok, %Activity{}}
iex> upsert_activity_speakers(activity, [1, 2, 3])
{:error, %Ecto.Changeset{}}
"""
def upsert_activity_speakers(%Activity{} = activity, speaker_ids) do
speakers =
Speaker
|> where([s], s.id in ^speaker_ids)
|> Repo.all()

activity
|> Activity.changeset_update_speakers(speakers)
|> Repo.update()
end

@doc """
Deletes a activity.
Expand Down Expand Up @@ -118,8 +150,6 @@ defmodule Safira.Activities do
Activity.changeset(activity, attrs)
end

alias Safira.Activities.ActivityCategory

@doc """
Returns the list of activity_categories.
Expand Down
11 changes: 11 additions & 0 deletions lib/safira/activities/activity.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ defmodule Safira.Activities.Activity do

belongs_to :category, Safira.Activities.ActivityCategory

many_to_many :speakers, Safira.Activities.Speaker,
join_through: "activities_speakers",
on_replace: :delete

timestamps(type: :utc_datetime)
end

Expand All @@ -44,4 +48,11 @@ defmodule Safira.Activities.Activity do
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
end

@doc false
def changeset_update_speakers(activity, speakers) do
activity
|> cast(%{}, @required_fields ++ @optional_fields)
|> put_assoc(:speakers, speakers)
end
end
4 changes: 4 additions & 0 deletions lib/safira/activities/speaker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ defmodule Safira.Activities.Speaker do

embeds_one :socials, Activities.Speaker.Socials

many_to_many :activities, Activities.Activity,
join_through: "activities_speakers",
on_replace: :delete

timestamps(type: :utc_datetime)
end

Expand Down
61 changes: 61 additions & 0 deletions lib/safira_web/components/forms.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule SafiraWeb.Components.Forms do
"""
use Phoenix.Component

import LiveSelect

alias Phoenix.HTML

@input_types ~w(
Expand Down Expand Up @@ -483,4 +485,63 @@ defmodule SafiraWeb.Components.Forms do

bin |> String.replace("_", " ") |> :string.titlecase()
end

attr :id, :any, default: nil, doc: "The id of the input. If not provided, it will be generated."
attr :name, :any, doc: "The name of the input. If not provided, it will be generated."
attr :class, :string, default: nil, doc: "The class to be added to the input."

attr :errors, :list,
default: [],
doc: "A list of erros to be displayed. If not provided, it will be generated."

attr :wrapper_class, :string, default: nil, doc: "The wrapper div class."
attr :label_class, :string, default: nil, doc: "Extra class for the label."
attr :help_text, :string, default: nil, doc: "Context/help for the input."

attr :required, :boolean,
default: false,
doc:
"If the input is required. In positive cases, it will add the `required` attribute to the input and a `*` to the label."

attr :target, :any, default: nil, doc: "The target for the live select component."

attr :field, HTML.FormField,
doc: "A form field struct retrieved from the form, for example: `@form[:email]`."

attr :rest, :global,
include: ~w(value_mapper placeholder),
doc: "Any other attribute to be added to the input."

def field_multiselect(assigns) do
~H"""
<.field_wrapper
errors={@errors}
name={Map.get(assigns, :name, @field.name)}
class={@wrapper_class}
>
<.field_label required={@required} for={@id} class={@label_class}>
<%= humanize(@field.field) %>
</.field_label>
<.live_select
id={assigns.id || @field.id}
mode={:tags}
field={@field}
phx-target={@target}
container_class={"#{@wrapper_class}"}
text_input_class="safira-text-input"
dropdown_class="safira-multiselect-dropdown"
option_class="safira-multiselect-dropdown-option"
selected_option_class="safira-multiselect-dropdown-option-selected"
tags_container_class="safira-multiselect-dropdown-tags-container"
tag_class="safira-multiselect-dropdown-tag"
clear_tag_button_class="safira-multiselect-dropdown-tag-remove"
{@rest}
/>
<.field_error :for={msg <- @errors}><%= msg %></.field_error>
<.field_help_text help_text={@help_text} />
</.field_wrapper>
"""
end
end
79 changes: 63 additions & 16 deletions lib/safira_web/live/backoffice/schedule_live/form_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule SafiraWeb.Backoffice.ScheduleLive.FormComponent do
use SafiraWeb, :live_component

alias Safira.Activities
alias Safira.Activities.Speaker
import SafiraWeb.Components.Forms

@impl true
Expand Down Expand Up @@ -45,13 +46,22 @@ defmodule SafiraWeb.Backoffice.ScheduleLive.FormComponent do
wrapper_class="col-span-1"
/>
</div>
<.field
field={@form[:category_id]}
type="select"
label="Category"
options={categories_options(@categories)}
wrapper_class="w-full"
/>
<div class="flex gap-2">
<.field
field={@form[:category_id]}
type="select"
label="Category"
options={categories_options(@categories)}
wrapper_class="w-full"
/>
<.field_multiselect
field={@form[:speakers]}
target={@myself}
value_mapper={&value_mapper/1}
wrapper_class="w-full"
placeholder={gettext("Search for speakers")}
/>
</div>
<div class="w-full flex gap-2">
<div class="w-full grid grid-cols-2">
<div class="w-full flex flex-col">
Expand Down Expand Up @@ -120,13 +130,40 @@ defmodule SafiraWeb.Backoffice.ScheduleLive.FormComponent do
save_activity(socket, socket.assigns.action, activity_params)
end

@impl true
def handle_event("live_select_change", %{"text" => text, "id" => live_select_id}, socket) do
case Activities.list_speakers(%{
"filters" => %{"1" => %{"field" => "name", "op" => "ilike_or", "value" => text}}
}) do
{:ok, {speakers, _meta}} ->
send_update(LiveSelect.Component,
id: live_select_id,
options: speakers |> Enum.map(&{&1.name, &1.id})
)

{:noreply, socket}

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

defp save_activity(socket, :edit, activity_params) do
case Activities.update_activity(socket.assigns.activity, activity_params) do
{:ok, _activity} ->
{:noreply,
socket
|> put_flash(:info, "Activity updated successfully")
|> push_patch(to: socket.assigns.patch)}
case Activities.upsert_activity_speakers(
socket.assigns.activity,
activity_params["speakers"]
) do
{:ok, _activity} ->
{:noreply,
socket
|> put_flash(:info, "Activity updated successfully")
|> push_patch(to: socket.assigns.patch)}

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
Expand All @@ -135,11 +172,17 @@ defmodule SafiraWeb.Backoffice.ScheduleLive.FormComponent do

defp save_activity(socket, :new, activity_params) do
case Activities.create_activity(activity_params) do
{:ok, _activity} ->
{:noreply,
socket
|> put_flash(:info, "Activity created successfully")
|> push_patch(to: socket.assigns.patch)}
{:ok, activity} ->
case Activities.upsert_activity_speakers(activity, activity_params["speakers"]) do
{:ok, _activity} ->
{:noreply,
socket
|> put_flash(:info, "Activity created successfully")
|> push_patch(to: socket.assigns.patch)}

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
Expand All @@ -150,4 +193,8 @@ defmodule SafiraWeb.Backoffice.ScheduleLive.FormComponent do
[{"None", nil}] ++
Enum.map(categories, &{&1.name, &1.id})
end

defp value_mapper(%Speaker{} = speaker), do: {speaker.name, speaker.id}

defp value_mapper(id), do: id
end
7 changes: 6 additions & 1 deletion lib/safira_web/live/backoffice/schedule_live/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ defmodule SafiraWeb.Backoffice.ScheduleLive.Index do
defp apply_action(socket, :speakers_edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit Speaker")
|> assign(:speaker, Activities.get_speaker!(id))
|> assign(
:speaker,
Map.update!(Activities.get_speaker!(id), :speakers, fn speakers ->
speakers |> Enum.map(&{&1.name, &1.id})
end)
)
end

defp apply_action(socket, :speakers_new, _params) do
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ defmodule Safira.MixProject do
compile: false,
depth: 1},
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
{:live_select, "~> 1.4"},

# monitoring
{:telemetry_metrics, "~> 1.0"},
Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"},
"jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"},
"live_select": {:hex, :live_select, "1.4.3", "ec9706952f589d8e2e6f98a0e1633c5b51ab5b807d503bd0d9622a26c999fb9a", [:mix], [{:ecto, "~> 3.8", [hex: :ecto, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.6.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "58f7d702b0f786c73d31e60a342c0a49afaf56ca5a6a078b51babf3490465220"},
"lua": {:hex, :lua, "0.0.14", "0f9f2b44271debdf855efe87583f73e874c4daec1e920c45a73d1fa8e3c2f9a8", [:mix], [{:luerl, "~> 1.2", [hex: :luerl, repo: "hexpm", optional: false]}], "hexpm", "9bd39736c349dd47541a5619925f00d7cf3f2a6d3d33248b80f9eac81f5850d3"},
"luerl": {:hex, :luerl, "1.2.0", "60f05f4240f0e7c148ddb79b67b8ff972734aad237aa74c83d0748b8214c8ef0", [:rebar3], [], "hexpm", "9cafd4f6094ff0f5a9d278fd81d60d3e026c820bdfb6cacd4b1bd909f21b525d"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
Expand All @@ -40,6 +41,7 @@
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"},
"phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
"phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.20.17", "f396bbdaf4ba227b82251eb75ac0afa6b3da5e509bc0d030206374237dfc9450", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61d741ffb78c85fdbca0de084da6a48f8ceb5261a79165b5a0b59e5f65ce98b"},
Expand Down
Loading

0 comments on commit a5e5417

Please sign in to comment.