From 2552a32865be818e65a8a854be2d140671cfe56e Mon Sep 17 00:00:00 2001 From: Chris McCord Date: Fri, 29 Oct 2021 12:12:23 -0400 Subject: [PATCH] UI function components --- assets/css/app.css | 6 +- lib/live_beats/media_library.ex | 46 +++++ lib/live_beats/media_library/genre.ex | 26 +++ lib/live_beats/media_library/song.ex | 24 +++ lib/live_beats_web.ex | 1 + lib/live_beats_web/controllers/user_auth.ex | 17 +- lib/live_beats_web/live/home_live.ex | 181 +++--------------- lib/live_beats_web/live/live_helpers.ex | 162 ++++++++++++---- lib/live_beats_web/live/modal_component.ex | 28 +++ lib/live_beats_web/live/nav.ex | 8 + lib/live_beats_web/live/player_live.ex | 4 +- .../live/{signin_live.ex => sign_in_live.ex} | 2 +- .../live/song_live/form_component.ex | 55 ++++++ .../live/song_live/form_component.html.heex | 40 ++++ lib/live_beats_web/live/song_live/index.ex | 79 ++++++++ lib/live_beats_web/live/song_live/show.ex | 21 ++ .../live/song_live/show.html.heex | 47 +++++ lib/live_beats_web/router.ex | 15 +- .../templates/layout/root.html.heex | 80 +++----- .../20210908150612_create_genres.exs | 4 + .../20211027201102_create_songs.exs | 22 +++ priv/repo/seeds.exs | 13 ++ test/live_beats/media_library_test.exs | 69 +++++++ test/live_beats_web/live/song_live_test.exs | 110 +++++++++++ .../fixtures/media_library_fixtures.ex | 25 +++ 25 files changed, 826 insertions(+), 259 deletions(-) create mode 100644 lib/live_beats/media_library.ex create mode 100644 lib/live_beats/media_library/genre.ex create mode 100644 lib/live_beats/media_library/song.ex create mode 100644 lib/live_beats_web/live/modal_component.ex create mode 100644 lib/live_beats_web/live/nav.ex rename lib/live_beats_web/live/{signin_live.ex => sign_in_live.ex} (97%) create mode 100644 lib/live_beats_web/live/song_live/form_component.ex create mode 100644 lib/live_beats_web/live/song_live/form_component.html.heex create mode 100644 lib/live_beats_web/live/song_live/index.ex create mode 100644 lib/live_beats_web/live/song_live/show.ex create mode 100644 lib/live_beats_web/live/song_live/show.html.heex create mode 100644 priv/repo/migrations/20211027201102_create_songs.exs create mode 100644 test/live_beats/media_library_test.exs create mode 100644 test/live_beats_web/live/song_live_test.exs create mode 100644 test/support/fixtures/media_library_fixtures.ex diff --git a/assets/css/app.css b/assets/css/app.css index 1d9f5a8..a7209a8 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -91,12 +91,10 @@ transition: opacity 1s ease-out; } -.phx-disconnected{ +.phx-loading{ cursor: wait; } -.phx-disconnected *{ - pointer-events: none; -} + .phx-modal { opacity: 1!important; diff --git a/lib/live_beats/media_library.ex b/lib/live_beats/media_library.ex new file mode 100644 index 0000000..9ea7532 --- /dev/null +++ b/lib/live_beats/media_library.ex @@ -0,0 +1,46 @@ +defmodule LiveBeats.MediaLibrary do + @moduledoc """ + The MediaLibrary context. + """ + + import Ecto.Query, warn: false + alias LiveBeats.Repo + + alias LiveBeats.MediaLibrary.{Song, Genre} + + def create_genre(attrs \\ %{}) do + %Genre{} + |> Genre.changeset(attrs) + |> Repo.insert() + end + + def list_genres do + Repo.all(Genre, order_by: [asc: :title]) + end + + def list_songs do + Repo.all(Song) + end + + def get_song!(id), do: Repo.get!(Song, id) + + def create_song(attrs \\ %{}) do + %Song{} + |> Song.changeset(attrs) + |> Repo.insert() + end + + def update_song(%Song{} = song, attrs) do + song + |> Song.changeset(attrs) + |> Repo.update() + end + + def delete_song(%Song{} = song) do + Repo.delete(song) + end + + def change_song(%Song{} = song, attrs \\ %{}) do + Song.changeset(song, attrs) + end +end diff --git a/lib/live_beats/media_library/genre.ex b/lib/live_beats/media_library/genre.ex new file mode 100644 index 0000000..bb15a61 --- /dev/null +++ b/lib/live_beats/media_library/genre.ex @@ -0,0 +1,26 @@ +defmodule LiveBeats.MediaLibrary.Genre do + use Ecto.Schema + import Ecto.Changeset + + schema "genres" do + field :title, :string + field :slug, :string + end + + @doc false + def changeset(song, attrs) do + song + |> cast(attrs, [:title]) + |> validate_required([:title]) + |> put_slug() + end + + defp put_slug(%Ecto.Changeset{valid?: false} = changeset), do: changeset + defp put_slug(%Ecto.Changeset{valid?: true} = changeset) do + if title = get_change(changeset, :title) do + put_change(changeset, :slug, Phoenix.Naming.underscore(title)) + else + changeset + end + end +end diff --git a/lib/live_beats/media_library/song.ex b/lib/live_beats/media_library/song.ex new file mode 100644 index 0000000..57f7e5c --- /dev/null +++ b/lib/live_beats/media_library/song.ex @@ -0,0 +1,24 @@ +defmodule LiveBeats.MediaLibrary.Song do + use Ecto.Schema + import Ecto.Changeset + + schema "songs" do + field :album_artist, :string + field :artist, :string + field :date_recorded, :naive_datetime + field :date_released, :naive_datetime + field :duration, :integer + field :title, :string + belongs_to :user, LiveBeats.Accounts.User + belongs_to :genre, LiveBeats.MediaLibrary.Genre + + timestamps() + end + + @doc false + def changeset(song, attrs) do + song + |> cast(attrs, [:album_artist, :artist, :duration, :title, :date_recorded, :date_released]) + |> validate_required([:artist, :duration, :title]) + end +end diff --git a/lib/live_beats_web.ex b/lib/live_beats_web.ex index 74d26a2..e41e284 100644 --- a/lib/live_beats_web.ex +++ b/lib/live_beats_web.ex @@ -91,6 +91,7 @@ defmodule LiveBeatsWeb do import LiveBeatsWeb.LiveHelpers import LiveBeatsWeb.Gettext alias LiveBeatsWeb.Router.Helpers, as: Routes + alias Phoenix.LiveView.JS end end diff --git a/lib/live_beats_web/controllers/user_auth.ex b/lib/live_beats_web/controllers/user_auth.ex index 780b895..92d46ed 100644 --- a/lib/live_beats_web/controllers/user_auth.ex +++ b/lib/live_beats_web/controllers/user_auth.ex @@ -6,8 +6,9 @@ defmodule LiveBeatsWeb.UserAuth do alias LiveBeats.Accounts alias LiveBeatsWeb.Router.Helpers, as: Routes - def on_mount(:default, _params, session, socket) do + def on_mount(:current_user, _params, session, socket) do socket = LiveView.assign(socket, :nonce, Map.fetch!(session, "nonce")) + case session do %{"user_id" => user_id} -> {:cont, LiveView.assign_new(socket, :current_user, fn -> Accounts.get_user!(user_id) end)} @@ -17,6 +18,19 @@ defmodule LiveBeatsWeb.UserAuth do end end + def on_mount(:ensure_authenticated, _params, session, socket) do + case session do + %{"user_id" => user_id} -> + {:cont, LiveView.assign_new(socket, :current_user, fn -> Accounts.get_user!(user_id) end)} + + %{} -> + {:halt, + socket + |> LiveView.put_flash(:error, "Please sign in") + |> LiveView.redirect(to: Routes.sign_in_path(socket, :index))} + end + end + @doc """ Logs the user in. @@ -102,6 +116,7 @@ defmodule LiveBeatsWeb.UserAuth do def require_authenticated_admin(conn, _opts) do user = conn.assigns[:current_user] + if user && LiveBeats.Accounts.admin?(user) do assign(conn, :current_admin, user) else diff --git a/lib/live_beats_web/live/home_live.ex b/lib/live_beats_web/live/home_live.ex index 2d6bf5e..19ea926 100644 --- a/lib/live_beats_web/live/home_live.ex +++ b/lib/live_beats_web/live/home_live.ex @@ -1,14 +1,19 @@ defmodule LiveBeatsWeb.HomeLive do use LiveBeatsWeb, :live_view + alias LiveBeats.MediaLibrary + def render(assigns) do ~H""" <.title_bar> LiveBeats - Chill - <:action>Share - <:action primary phx-click={show_modal("add-songs")}>Add Songs + <:actions> + <.button>Share + <.button primary phx-click={show_modal("add-songs")}>Add Songs + + <.modal id="add-songs"> <:title>Add Songs a modal @@ -321,164 +326,26 @@ defmodule LiveBeatsWeb.HomeLive do - + <.table rows={@songs}> + <:col let={song} label="Song"> + <%= song.title %> + + <:col let={song} label="Artist"> + <%= song.artist %> + + <:col let={song} label="Time"> + <%= song.duration %> + + <:col label=""> + """ end def mount(_parmas, _session, socket) do - {:ok, socket} + {:ok, assign(socket, :songs, fetch_songs(socket))} + end + + defp fetch_songs(_socket) do + MediaLibrary.list_songs() end end diff --git a/lib/live_beats_web/live/live_helpers.ex b/lib/live_beats_web/live/live_helpers.ex index 4b05c29..3968186 100644 --- a/lib/live_beats_web/live/live_helpers.ex +++ b/lib/live_beats_web/live/live_helpers.ex @@ -1,16 +1,33 @@ defmodule LiveBeatsWeb.LiveHelpers do import Phoenix.LiveView import Phoenix.LiveView.Helpers - import Phoenix.HTML alias Phoenix.LiveView.JS - def link_patch(assigns) do + def link(%{redirect_to: to} = assigns) do + opts = assigns |> assigns_to_attributes() |> Keyword.put(:to, to) + assigns = assign(assigns, :opts, opts) + ~H""" - <%= live_patch @text, [ - to: @to, - class: "phx-modal-close" - ] ++ assigns_to_attributes(assigns, [:to, :text]) %> + <%= live_redirect @opts do %><%= render_slot(@inner_block) %><% end %> + """ + end + + def link(%{patch_to: to} = assigns) do + opts = assigns |> assigns_to_attributes() |> Keyword.put(:to, to) + assigns = assign(assigns, :opts, opts) + + ~H""" + <%= live_patch @opts do %><%= render_slot(@inner_block) %><% end %> + """ + end + + def link(%{} = assigns) do + opts = assigns |> assigns_to_attributes() |> Keyword.put(:to, assigns[:href] || "#") + assigns = assign(assigns, :opts, opts) + + ~H""" + <%= Phoenix.HTML.Link.link @opts do %><%= render_slot(@inner_block) %><% end %> """ end @@ -63,7 +80,7 @@ defmodule LiveBeatsWeb.LiveHelpers do transition: {"ease-out duration-300", "opacity-0", "opacity-100"} ) |> JS.show( - to: "##{id} .modal-content", + to: "##{id}-content", display: "inline-block", transition: {"ease-out duration-300", "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95", @@ -73,6 +90,7 @@ defmodule LiveBeatsWeb.LiveHelpers do def hide_modal(js \\ %JS{}, id) do js + |> JS.remove_class("fade-in", to: "##{id}") |> JS.hide( to: "##{id}", transition: {"ease-in duration-200", "opacity-100", "opacity-0"} @@ -83,26 +101,35 @@ defmodule LiveBeatsWeb.LiveHelpers do {"ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100", "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"} ) + |> JS.dispatch("click", to: "##{id} [data-modal-return]") end def modal(assigns) do assigns = assigns - |> assign_new(:title, fn -> "" end) + |> assign_new(:show, fn -> false end) + |> assign_new(:title, fn -> [] end) + |> assign_new(:confirm, fn -> nil end) + |> assign_new(:cancel, fn -> nil end) |> assign_new(:return_to, fn -> nil end) ~H""" - @@ -246,8 +230,8 @@ Home <% end %> - + <%= live_redirect to: Routes.song_index_path(@conn, :index), + class: "text-gray-700 hover:text-gray-900 hover:bg-gray-50 group flex items-center px-2 py-2 text-sm font-medium rounded-md" do %> My Songs - + <% end %> <%= unless @current_user do %> - <%= live_redirect to: Routes.signin_path(@conn, :index), + <%= live_redirect to: Routes.sign_in_path(@conn, :index), class: "text-gray-700 hover:text-gray-900 hover:bg-gray-50 group flex items-center px-2 py-2 text-sm font-medium rounded-md" do %>
- - - - - Chill - - - - - - - Pop - - - - - - - Techo - - - + <%= for genre <- @genres do %> + + + + <%= genre.title %> + + + <% end %>
diff --git a/priv/repo/migrations/20210908150612_create_genres.exs b/priv/repo/migrations/20210908150612_create_genres.exs index e160dd2..4edb5bc 100644 --- a/priv/repo/migrations/20210908150612_create_genres.exs +++ b/priv/repo/migrations/20210908150612_create_genres.exs @@ -4,6 +4,10 @@ defmodule LiveBeats.Repo.Migrations.CreateGenres do def change do create table(:genres) do add :title, :text, null: false + add :slug, :text, null: false end + + create unique_index(:genres, [:title]) + create unique_index(:genres, [:slug]) end end diff --git a/priv/repo/migrations/20211027201102_create_songs.exs b/priv/repo/migrations/20211027201102_create_songs.exs new file mode 100644 index 0000000..9bff53a --- /dev/null +++ b/priv/repo/migrations/20211027201102_create_songs.exs @@ -0,0 +1,22 @@ +defmodule LiveBeats.Repo.Migrations.CreateSongs do + use Ecto.Migration + + def change do + create table(:songs) do + add :album_artist, :string + add :artist, :string + add :duration, :integer + add :title, :string + add :date_recorded, :naive_datetime + add :date_released, :naive_datetime + add :user_id, references(:users, on_delete: :nothing) + add :genre_id, references(:genres, on_delete: :nothing) + + timestamps() + end + + create unique_index(:songs, [:user_id, :title, :artist]) + create index(:songs, [:user_id]) + create index(:songs, [:genre_id]) + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index c553933..f40545c 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -9,3 +9,16 @@ # # We recommend using the bang functions (`insert!`, `update!` # and so on) as they will fail if something goes wrong. + +for title <- ~w(Chill Pop Hip-hop Electronic) do + {:ok, _} = LiveBeats.MediaLibrary.create_genre(%{title: title}) +end + +for i <- 1..20 do + {:ok, _} = + LiveBeats.MediaLibrary.create_song(%{ + artist: "Bonobo", + title: "Black Sands #{i}", + duration: 180_000 + }) +end diff --git a/test/live_beats/media_library_test.exs b/test/live_beats/media_library_test.exs new file mode 100644 index 0000000..a9ab49a --- /dev/null +++ b/test/live_beats/media_library_test.exs @@ -0,0 +1,69 @@ +defmodule LiveBeats.MediaLibraryTest do + use LiveBeats.DataCase + + alias LiveBeats.MediaLibrary + + describe "songs" do + alias LiveBeats.MediaLibrary.Song + + import LiveBeats.MediaLibraryFixtures + + @invalid_attrs %{album_artist: nil, artist: nil, date_recorded: nil, date_released: nil, duration: nil, title: nil} + + test "list_songs/0 returns all songs" do + song = song_fixture() + assert MediaLibrary.list_songs() == [song] + end + + test "get_song!/1 returns the song with given id" do + song = song_fixture() + assert MediaLibrary.get_song!(song.id) == song + end + + test "create_song/1 with valid data creates a song" do + valid_attrs = %{album_artist: "some album_artist", artist: "some artist", date_recorded: ~N[2021-10-26 20:11:00], date_released: ~N[2021-10-26 20:11:00], duration: 42, title: "some title"} + + assert {:ok, %Song{} = song} = MediaLibrary.create_song(valid_attrs) + assert song.album_artist == "some album_artist" + assert song.artist == "some artist" + assert song.date_recorded == ~N[2021-10-26 20:11:00] + assert song.date_released == ~N[2021-10-26 20:11:00] + assert song.duration == 42 + assert song.title == "some title" + end + + test "create_song/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = MediaLibrary.create_song(@invalid_attrs) + end + + test "update_song/2 with valid data updates the song" do + song = song_fixture() + update_attrs = %{album_artist: "some updated album_artist", artist: "some updated artist", date_recorded: ~N[2021-10-27 20:11:00], date_released: ~N[2021-10-27 20:11:00], duration: 43, title: "some updated title"} + + assert {:ok, %Song{} = song} = MediaLibrary.update_song(song, update_attrs) + assert song.album_artist == "some updated album_artist" + assert song.artist == "some updated artist" + assert song.date_recorded == ~N[2021-10-27 20:11:00] + assert song.date_released == ~N[2021-10-27 20:11:00] + assert song.duration == 43 + assert song.title == "some updated title" + end + + test "update_song/2 with invalid data returns error changeset" do + song = song_fixture() + assert {:error, %Ecto.Changeset{}} = MediaLibrary.update_song(song, @invalid_attrs) + assert song == MediaLibrary.get_song!(song.id) + end + + test "delete_song/1 deletes the song" do + song = song_fixture() + assert {:ok, %Song{}} = MediaLibrary.delete_song(song) + assert_raise Ecto.NoResultsError, fn -> MediaLibrary.get_song!(song.id) end + end + + test "change_song/1 returns a song changeset" do + song = song_fixture() + assert %Ecto.Changeset{} = MediaLibrary.change_song(song) + end + end +end diff --git a/test/live_beats_web/live/song_live_test.exs b/test/live_beats_web/live/song_live_test.exs new file mode 100644 index 0000000..b0d6cfe --- /dev/null +++ b/test/live_beats_web/live/song_live_test.exs @@ -0,0 +1,110 @@ +defmodule LiveBeatsWeb.SongLiveTest do + use LiveBeatsWeb.ConnCase + + import Phoenix.LiveViewTest + import LiveBeats.MediaLibraryFixtures + + @create_attrs %{album_artist: "some album_artist", artist: "some artist", date_recorded: %{day: 26, hour: 20, minute: 11, month: 10, year: 2021}, date_released: %{day: 26, hour: 20, minute: 11, month: 10, year: 2021}, duration: 42, title: "some title"} + @update_attrs %{album_artist: "some updated album_artist", artist: "some updated artist", date_recorded: %{day: 27, hour: 20, minute: 11, month: 10, year: 2021}, date_released: %{day: 27, hour: 20, minute: 11, month: 10, year: 2021}, duration: 43, title: "some updated title"} + @invalid_attrs %{album_artist: nil, artist: nil, date_recorded: %{day: 30, hour: 20, minute: 11, month: 2, year: 2021}, date_released: %{day: 30, hour: 20, minute: 11, month: 2, year: 2021}, duration: nil, title: nil} + + defp create_song(_) do + song = song_fixture() + %{song: song} + end + + describe "Index" do + setup [:create_song] + + test "lists all songs", %{conn: conn, song: song} do + {:ok, _index_live, html} = live(conn, Routes.song_index_path(conn, :index)) + + assert html =~ "Listing Songs" + assert html =~ song.album_artist + end + + test "saves new song", %{conn: conn} do + {:ok, index_live, _html} = live(conn, Routes.song_index_path(conn, :index)) + + assert index_live |> element("a", "New Song") |> render_click() =~ + "New Song" + + assert_patch(index_live, Routes.song_index_path(conn, :new)) + + assert index_live + |> form("#song-form", song: @invalid_attrs) + |> render_change() =~ "is invalid" + + {:ok, _, html} = + index_live + |> form("#song-form", song: @create_attrs) + |> render_submit() + |> follow_redirect(conn, Routes.song_index_path(conn, :index)) + + assert html =~ "Song created successfully" + assert html =~ "some album_artist" + end + + test "updates song in listing", %{conn: conn, song: song} do + {:ok, index_live, _html} = live(conn, Routes.song_index_path(conn, :index)) + + assert index_live |> element("#song-#{song.id} a", "Edit") |> render_click() =~ + "Edit Song" + + assert_patch(index_live, Routes.song_index_path(conn, :edit, song)) + + assert index_live + |> form("#song-form", song: @invalid_attrs) + |> render_change() =~ "is invalid" + + {:ok, _, html} = + index_live + |> form("#song-form", song: @update_attrs) + |> render_submit() + |> follow_redirect(conn, Routes.song_index_path(conn, :index)) + + assert html =~ "Song updated successfully" + assert html =~ "some updated album_artist" + end + + test "deletes song in listing", %{conn: conn, song: song} do + {:ok, index_live, _html} = live(conn, Routes.song_index_path(conn, :index)) + + assert index_live |> element("#song-#{song.id} a", "Delete") |> render_click() + refute has_element?(index_live, "#song-#{song.id}") + end + end + + describe "Show" do + setup [:create_song] + + test "displays song", %{conn: conn, song: song} do + {:ok, _show_live, html} = live(conn, Routes.song_show_path(conn, :show, song)) + + assert html =~ "Show Song" + assert html =~ song.album_artist + end + + test "updates song within modal", %{conn: conn, song: song} do + {:ok, show_live, _html} = live(conn, Routes.song_show_path(conn, :show, song)) + + assert show_live |> element("a", "Edit") |> render_click() =~ + "Edit Song" + + assert_patch(show_live, Routes.song_show_path(conn, :edit, song)) + + assert show_live + |> form("#song-form", song: @invalid_attrs) + |> render_change() =~ "is invalid" + + {:ok, _, html} = + show_live + |> form("#song-form", song: @update_attrs) + |> render_submit() + |> follow_redirect(conn, Routes.song_show_path(conn, :show, song)) + + assert html =~ "Song updated successfully" + assert html =~ "some updated album_artist" + end + end +end diff --git a/test/support/fixtures/media_library_fixtures.ex b/test/support/fixtures/media_library_fixtures.ex new file mode 100644 index 0000000..dfcd669 --- /dev/null +++ b/test/support/fixtures/media_library_fixtures.ex @@ -0,0 +1,25 @@ +defmodule LiveBeats.MediaLibraryFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `LiveBeats.MediaLibrary` context. + """ + + @doc """ + Generate a song. + """ + def song_fixture(attrs \\ %{}) do + {:ok, song} = + attrs + |> Enum.into(%{ + album_artist: "some album_artist", + artist: "some artist", + date_recorded: ~N[2021-10-26 20:11:00], + date_released: ~N[2021-10-26 20:11:00], + duration: 42, + title: "some title" + }) + |> LiveBeats.MediaLibrary.create_song() + + song + end +end