mirror of
https://github.com/fly-apps/live_beats.git
synced 2024-11-21 15:41:00 +00:00
UI function components
This commit is contained in:
parent
708bf715e1
commit
2552a32865
25 changed files with 826 additions and 259 deletions
|
@ -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;
|
||||
|
|
46
lib/live_beats/media_library.ex
Normal file
46
lib/live_beats/media_library.ex
Normal file
|
@ -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
|
26
lib/live_beats/media_library/genre.ex
Normal file
26
lib/live_beats/media_library/genre.ex
Normal file
|
@ -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
|
24
lib/live_beats/media_library/song.ex
Normal file
24
lib/live_beats/media_library/song.ex
Normal file
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
<:action primary phx-click={show_modal("add-songs")}>Add Songs</:action>
|
||||
<:actions>
|
||||
<.button>Share</.button>
|
||||
<.button primary phx-click={show_modal("add-songs")}>Add Songs</.button>
|
||||
</:actions>
|
||||
</.title_bar>
|
||||
|
||||
<.modal id="add-songs">
|
||||
<:title>Add Songs</:title>
|
||||
a modal
|
||||
|
@ -321,164 +326,26 @@ defmodule LiveBeatsWeb.HomeLive do
|
|||
</div>
|
||||
|
||||
<!-- Songs table (small breakpoint and up) -->
|
||||
<div class="hidden mt-8 sm:block">
|
||||
<div class="align-middle inline-block min-w-full border-b border-gray-200">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr class="border-t border-gray-200">
|
||||
<th
|
||||
class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<span class="lg:pl-2">Nextup</span>
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
likes
|
||||
</th>
|
||||
<th
|
||||
class="hidden md:table-cell px-6 py-3 border-b border-gray-200 bg-gray-50 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
user
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-100">
|
||||
<%= for _ <- 1..20 do %>
|
||||
<tr>
|
||||
<td class="px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
<div class="flex items-center space-x-3 lg:pl-2">
|
||||
<div class="flex-shrink-0 w-2.5 h-2.5 rounded-full bg-pink-600" aria-hidden="true"></div>
|
||||
<a href="#" class="truncate hover:text-gray-600">
|
||||
<span>
|
||||
GraphQL API
|
||||
<!-- space -->
|
||||
<span class="text-gray-500 font-normal">in Engineering</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-3 text-sm text-gray-500 font-medium">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex flex-shrink-0 -space-x-1">
|
||||
|
||||
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Dries Vincent">
|
||||
|
||||
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="https://images.unsplash.com/photo-1517841905240-472988babdf9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Lindsay Walton">
|
||||
|
||||
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Courtney Henry">
|
||||
|
||||
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Tom Cook">
|
||||
|
||||
</div>
|
||||
|
||||
<span class="flex-shrink-0 text-xs leading-5 font-medium">+8</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden md:table-cell px-6 py-3 whitespace-nowrap text-sm text-gray-500 text-right">
|
||||
March 17, 2020
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
|
||||
<tr>
|
||||
<td class="px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
<div class="flex items-center space-x-3 lg:pl-2">
|
||||
<div class="flex-shrink-0 w-2.5 h-2.5 rounded-full bg-purple-600" aria-hidden="true"></div>
|
||||
<a href="#" class="truncate hover:text-gray-600">
|
||||
<span>
|
||||
New Benefits Plan
|
||||
<!-- space -->
|
||||
<span class="text-gray-500 font-normal">in Human Resources</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-3 text-sm text-gray-500 font-medium">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex flex-shrink-0 -space-x-1">
|
||||
|
||||
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="https://images.unsplash.com/photo-1519345182560-3f2917c472ef?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Leonard Krasner">
|
||||
|
||||
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="https://images.unsplash.com/photo-1463453091185-61582044d556?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Floyd Miles">
|
||||
|
||||
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="https://images.unsplash.com/photo-1502685104226-ee32379fefbe?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Emily Selman">
|
||||
|
||||
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="https://images.unsplash.com/photo-1500917293891-ef795e70e1f6?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Kristin Watson">
|
||||
|
||||
</div>
|
||||
|
||||
<span class="flex-shrink-0 text-xs leading-5 font-medium">+4</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden md:table-cell px-6 py-3 whitespace-nowrap text-sm text-gray-500 text-right">
|
||||
April 4, 2020
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
<div class="flex items-center space-x-3 lg:pl-2">
|
||||
<div class="flex-shrink-0 w-2.5 h-2.5 rounded-full bg-yellow-500" aria-hidden="true"></div>
|
||||
<a href="#" class="truncate hover:text-gray-600">
|
||||
<span>
|
||||
Onboarding Emails
|
||||
<!-- space -->
|
||||
<span class="text-gray-500 font-normal">in Customer Success</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-3 text-sm text-gray-500 font-medium">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex flex-shrink-0 -space-x-1">
|
||||
|
||||
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="https://images.unsplash.com/photo-1502685104226-ee32379fefbe?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Emily Selman">
|
||||
|
||||
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="https://images.unsplash.com/photo-1500917293891-ef795e70e1f6?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Kristin Watson">
|
||||
|
||||
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="https://images.unsplash.com/photo-1505840717430-882ce147ef2d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Emma Dorsey">
|
||||
|
||||
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white"
|
||||
src="https://images.unsplash.com/photo-1509783236416-c9ad59bae472?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
alt="Alicia Bell">
|
||||
|
||||
</div>
|
||||
|
||||
<span class="flex-shrink-0 text-xs leading-5 font-medium">+10</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden md:table-cell px-6 py-3 whitespace-nowrap text-sm text-gray-500 text-right">
|
||||
March 30, 2020
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<.table rows={@songs}>
|
||||
<:col let={song} label="Song">
|
||||
<%= song.title %>
|
||||
</:col>
|
||||
<:col let={song} label="Artist">
|
||||
<%= song.artist %>
|
||||
</:col>
|
||||
<:col let={song} label="Time">
|
||||
<%= song.duration %>
|
||||
</:col>
|
||||
<:col label=""></:col>
|
||||
</.table>
|
||||
"""
|
||||
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
|
||||
|
|
|
@ -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"""
|
||||
<div id={@id} class="hidden fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div id={@id} class={"fixed z-10 inset-0 overflow-y-auto #{if @show, do: "fade-in", else: "hidden"}"} aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
<div
|
||||
class="modal-content inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6"
|
||||
id={"#{@id}-content"}
|
||||
class={"#{if @show, do: "fade-in-scale", else: "hidden"} modal-content inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6"}
|
||||
phx-window-keydown={hide_modal(@id)} phx-key="escape"
|
||||
phx-click-away={hide_modal(@id)}
|
||||
>
|
||||
<%= if @return_to do %>
|
||||
<%= live_redirect "close", to: @return_to, data: [modal_return: true], class: "hidden" %>
|
||||
<% end %>
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-green-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<!-- Heroicon name: outline/exclamation -->
|
||||
<!-- Heroicon name: outline/plus -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
@ -119,17 +146,20 @@ defmodule LiveBeatsWeb.LiveHelpers do
|
|||
</div>
|
||||
</div>
|
||||
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<button type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
<%= render_slot(@confirm) %>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm"
|
||||
phx-window-keydown={hide_modal(@id)} phx-key="escape"
|
||||
phx-click={hide_modal(@id)}
|
||||
>
|
||||
<%= render_slot(@cancel) %>
|
||||
</button>
|
||||
<%= if @confirm do %>
|
||||
<button type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
<%= render_slot(@confirm) %>
|
||||
</button>
|
||||
<% end %>
|
||||
<%= if @cancel do %>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm"
|
||||
phx-click={hide_modal(@id)}
|
||||
>
|
||||
<%= render_slot(@cancel) %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -158,28 +188,90 @@ defmodule LiveBeatsWeb.LiveHelpers do
|
|||
end
|
||||
|
||||
def title_bar(assigns) do
|
||||
assigns = assign_new(assigns, :action, fn -> [] end)
|
||||
assigns = assign_new(assigns, :actions, fn -> [] end)
|
||||
|
||||
~H"""
|
||||
<!-- Page title & actions -->
|
||||
<div class="border-b border-gray-200 px-4 py-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8">
|
||||
<div class="border-b border-gray-200 px-4 py-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8 h-16">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="text-lg font-medium leading-6 text-gray-900 sm:truncate">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="mt-4 flex sm:mt-0 sm:ml-4">
|
||||
<%= for action <- @action, rest = assigns_to_attributes(action) do %>
|
||||
<%= if action[:primary] do %>
|
||||
<button type="button" class="order-0 inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-1 sm:ml-3" {rest}>
|
||||
<%= render_slot(action) %>
|
||||
</button>
|
||||
<% else %>
|
||||
<button type="button" class="order-1 inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-0 sm:ml-0 lg:ml-3" {rest}>
|
||||
<%= render_slot(action) %>
|
||||
</button>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render_slot(@actions) %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def button(%{patch_to: _} = assigns) do
|
||||
assigns = assign_new(assigns, :primary, fn -> false end)
|
||||
|
||||
~H"""
|
||||
<%= if @primary do %>
|
||||
<%= live_patch to: @patch_to, class: "order-0 inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-1 sm:ml-3" do %>
|
||||
<%= render_slot(@inner_block) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= live_patch to: @patch_to, class: "order-1 inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-0 sm:ml-0 lg:ml-3" do %>
|
||||
<%= render_slot(@inner_block) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
def button(%{} = assigns) do
|
||||
assigns =
|
||||
assigns
|
||||
|> assign_new(:primary, fn -> false end)
|
||||
|> assign(:rest, assigns_to_attributes(assigns))
|
||||
|
||||
~H"""
|
||||
<%= if @primary do %>
|
||||
<button type="button" class="order-0 inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-1 sm:ml-3" {@rest}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</button>
|
||||
<% else %>
|
||||
<button type="button" class="order-1 inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-0 sm:ml-0 lg:ml-3" {@rest}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</button>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
def table(assigns) do
|
||||
~H"""
|
||||
<div class="hidden mt-8 sm:block">
|
||||
<div class="align-middle inline-block min-w-full border-b border-gray-200">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr class="border-t border-gray-200">
|
||||
<%= for col <- @col do %>
|
||||
<th
|
||||
class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<span class="lg:pl-2"><%= col.label %></span>
|
||||
</th>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-100">
|
||||
<%= for row <- @rows do %>
|
||||
<tr class="hover:bg-gray-50">
|
||||
<%= for {col, i} <- Enum.with_index(@col) do %>
|
||||
<td class={"px-6 py-3 whitespace-nowrap text-sm font-medium text-gray-900 #{if i == 0, do: "max-w-0 w-full"}"}>
|
||||
<div class="flex items-center space-x-3 lg:pl-2">
|
||||
<%= if i == 0 do %>
|
||||
<div class="flex-shrink-0 w-2.5 h-2.5 rounded-full bg-pink-600 mr-2" aria-hidden="true"></div>
|
||||
<% end %>
|
||||
<%= render_slot(col, row) %>
|
||||
</div>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
|
28
lib/live_beats_web/live/modal_component.ex
Normal file
28
lib/live_beats_web/live/modal_component.ex
Normal file
|
@ -0,0 +1,28 @@
|
|||
defmodule LiveBeatsWeb.ModalComponent do
|
||||
use LiveBeatsWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id={@id}
|
||||
class="phx-modal"
|
||||
phx-capture-click="close"
|
||||
phx-window-keydown="close"
|
||||
phx-key="escape"
|
||||
phx-target={@myself}
|
||||
phx-page-loading>
|
||||
|
||||
<div class="phx-modal-content">
|
||||
<%= live_patch raw("×"), to: @return_to, class: "phx-modal-close" %>
|
||||
<%= live_component @component, @opts %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("close", _, socket) do
|
||||
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
|
||||
end
|
||||
end
|
8
lib/live_beats_web/live/nav.ex
Normal file
8
lib/live_beats_web/live/nav.ex
Normal file
|
@ -0,0 +1,8 @@
|
|||
defmodule LiveBeatsWeb.Nav do
|
||||
import Phoenix.LiveView
|
||||
import Phoenix.LiveView.Helpers
|
||||
|
||||
def on_mount(:default, _params, session, socket) do
|
||||
{:cont, assign(socket, genres: LiveBeats.MediaLibrary.list_genres())}
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
defmodule LiveBeatsWeb.PlayerLive do
|
||||
use LiveBeatsWeb, :live_view
|
||||
|
||||
on_mount LiveBeatsWeb.UserAuth
|
||||
on_mount {LiveBeatsWeb.UserAuth, :current_user}
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
|
@ -76,7 +76,7 @@ defmodule LiveBeatsWeb.PlayerLive do
|
|||
end
|
||||
|
||||
def mount(_parmas, _session, socket) do
|
||||
if connected?(socket), do: Process.send_after(self(), :tick, 1000)
|
||||
# if connected?(socket), do: Process.send_after(self(), :tick, 1000)
|
||||
{:ok, assign(socket, time: inspect(System.system_time()), count: 0), layout: false}
|
||||
end
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
defmodule LiveBeatsWeb.SigninLive do
|
||||
defmodule LiveBeatsWeb.SignInLive do
|
||||
use LiveBeatsWeb, :live_view
|
||||
|
||||
def render(assigns) do
|
55
lib/live_beats_web/live/song_live/form_component.ex
Normal file
55
lib/live_beats_web/live/song_live/form_component.ex
Normal file
|
@ -0,0 +1,55 @@
|
|||
defmodule LiveBeatsWeb.SongLive.FormComponent do
|
||||
use LiveBeatsWeb, :live_component
|
||||
|
||||
alias LiveBeats.MediaLibrary
|
||||
|
||||
@impl true
|
||||
def update(%{song: song} = assigns, socket) do
|
||||
changeset = MediaLibrary.change_song(song)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(:changeset, changeset)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"song" => song_params}, socket) do
|
||||
changeset =
|
||||
socket.assigns.song
|
||||
|> MediaLibrary.change_song(song_params)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
{:noreply, assign(socket, :changeset, changeset)}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"song" => song_params}, socket) do
|
||||
save_song(socket, socket.assigns.action, song_params)
|
||||
end
|
||||
|
||||
defp save_song(socket, :edit, song_params) do
|
||||
case MediaLibrary.update_song(socket.assigns.song, song_params) do
|
||||
{:ok, _song} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Song updated successfully")
|
||||
|> push_redirect(to: socket.assigns.return_to)}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, :changeset, changeset)}
|
||||
end
|
||||
end
|
||||
|
||||
defp save_song(socket, :new, song_params) do
|
||||
case MediaLibrary.create_song(song_params) do
|
||||
{:ok, _song} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Song created successfully")
|
||||
|> push_redirect(to: socket.assigns.return_to)}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, changeset: changeset)}
|
||||
end
|
||||
end
|
||||
end
|
40
lib/live_beats_web/live/song_live/form_component.html.heex
Normal file
40
lib/live_beats_web/live/song_live/form_component.html.heex
Normal file
|
@ -0,0 +1,40 @@
|
|||
<div>
|
||||
<h2><%= @title %></h2>
|
||||
|
||||
<.form
|
||||
let={f}
|
||||
for={@changeset}
|
||||
id="song-form"
|
||||
phx-target={@myself}
|
||||
phx-change="validate"
|
||||
phx-submit="save">
|
||||
|
||||
<%= label f, :album_artist %>
|
||||
<%= text_input f, :album_artist %>
|
||||
<%= error_tag f, :album_artist %>
|
||||
|
||||
<%= label f, :artist %>
|
||||
<%= text_input f, :artist %>
|
||||
<%= error_tag f, :artist %>
|
||||
|
||||
<%= label f, :duration %>
|
||||
<%= number_input f, :duration %>
|
||||
<%= error_tag f, :duration %>
|
||||
|
||||
<%= label f, :title %>
|
||||
<%= text_input f, :title %>
|
||||
<%= error_tag f, :title %>
|
||||
|
||||
<%= label f, :date_recorded %>
|
||||
<%= datetime_select f, :date_recorded %>
|
||||
<%= error_tag f, :date_recorded %>
|
||||
|
||||
<%= label f, :date_released %>
|
||||
<%= datetime_select f, :date_released %>
|
||||
<%= error_tag f, :date_released %>
|
||||
|
||||
<div>
|
||||
<%= submit "Save", phx_disable_with: "Saving..." %>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
79
lib/live_beats_web/live/song_live/index.ex
Normal file
79
lib/live_beats_web/live/song_live/index.ex
Normal file
|
@ -0,0 +1,79 @@
|
|||
defmodule LiveBeatsWeb.SongLive.Index do
|
||||
use LiveBeatsWeb, :live_view
|
||||
|
||||
alias LiveBeats.MediaLibrary
|
||||
alias LiveBeats.MediaLibrary.Song
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.title_bar>
|
||||
Listing Songs
|
||||
|
||||
<:actions>
|
||||
<.button primary patch_to={Routes.song_index_path(@socket, :new)}>New Song</.button>
|
||||
</:actions>
|
||||
</.title_bar>
|
||||
|
||||
<%= if @live_action in [:new, :edit] do %>
|
||||
<.modal show id="add-songs" return_to={Routes.song_index_path(@socket, :index)}>
|
||||
<.live_component
|
||||
module={LiveBeatsWeb.SongLive.FormComponent}
|
||||
title={@page_title}
|
||||
id={@song.id || :new}
|
||||
action={@live_action}
|
||||
return_to={Routes.song_index_path(@socket, :index)}
|
||||
song={@song}
|
||||
/>
|
||||
</.modal>
|
||||
<% end %>
|
||||
|
||||
<.table rows={@songs}>
|
||||
<:col let={song} label="Title"><%= song.title %></:col>
|
||||
<:col let={song} label="Artist"><%= song.artist %></:col>
|
||||
<:col let={song} label="Duration"><%= song.duration %></:col>
|
||||
<:col let={song} label="">
|
||||
<.link redirect_to={Routes.song_show_path(@socket, :show, song)}>Show</.link>
|
||||
<.link patch_to={Routes.song_index_path(@socket, :edit, song)}>Edit</.link>
|
||||
<.link phx-click={JS.push("delete", value: %{id: song.id})} data-confirm="Are you sure?">Delete</.link>
|
||||
</:col>
|
||||
</.table>
|
||||
"""
|
||||
end
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, assign(socket, :songs, list_songs())}
|
||||
end
|
||||
|
||||
def handle_params(params, _url, socket) do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||
socket
|
||||
|> assign(:page_title, "Edit Song")
|
||||
|> assign(:song, MediaLibrary.get_song!(id))
|
||||
end
|
||||
|
||||
defp apply_action(socket, :new, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "New Song")
|
||||
|> assign(:song, %Song{})
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "Listing Songs")
|
||||
|> assign(:song, nil)
|
||||
end
|
||||
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
song = MediaLibrary.get_song!(id)
|
||||
{:ok, _} = MediaLibrary.delete_song(song)
|
||||
|
||||
{:noreply, assign(socket, :songs, list_songs())}
|
||||
end
|
||||
|
||||
defp list_songs do
|
||||
MediaLibrary.list_songs()
|
||||
end
|
||||
end
|
21
lib/live_beats_web/live/song_live/show.ex
Normal file
21
lib/live_beats_web/live/song_live/show.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule LiveBeatsWeb.SongLive.Show do
|
||||
use LiveBeatsWeb, :live_view
|
||||
|
||||
alias LiveBeats.MediaLibrary
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||
|> assign(:song, MediaLibrary.get_song!(id))}
|
||||
end
|
||||
|
||||
defp page_title(:show), do: "Show Song"
|
||||
defp page_title(:edit), do: "Edit Song"
|
||||
end
|
47
lib/live_beats_web/live/song_live/show.html.heex
Normal file
47
lib/live_beats_web/live/song_live/show.html.heex
Normal file
|
@ -0,0 +1,47 @@
|
|||
<h1>Show Song</h1>
|
||||
|
||||
<%= if @live_action in [:edit] do %>
|
||||
<%#= live_modal LiveBeatsWeb.SongLive.FormComponent,
|
||||
id: @song.id,
|
||||
title: @page_title,
|
||||
action: @live_action,
|
||||
song: @song,
|
||||
return_to: Routes.song_show_path(@socket, :show, @song) %>
|
||||
<% end %>
|
||||
|
||||
<ul>
|
||||
|
||||
<li>
|
||||
<strong>Album artist:</strong>
|
||||
<%= @song.album_artist %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Artist:</strong>
|
||||
<%= @song.artist %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Duration:</strong>
|
||||
<%= @song.duration %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Title:</strong>
|
||||
<%= @song.title %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Date recorded:</strong>
|
||||
<%= @song.date_recorded %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Date released:</strong>
|
||||
<%= @song.date_released %>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<span><%= live_patch "Edit", to: Routes.song_show_path(@socket, :edit, @song), class: "button" %></span> |
|
||||
<span><%= live_redirect "Back", to: Routes.song_index_path(@socket, :index) %></span>
|
|
@ -20,10 +20,19 @@ defmodule LiveBeatsWeb.Router do
|
|||
scope "/", LiveBeatsWeb do
|
||||
pipe_through :browser
|
||||
|
||||
live_session :default, on_mount: LiveBeatsWeb.UserAuth do
|
||||
delete "/signout", OAuthCallbackController, :sign_out
|
||||
|
||||
live_session :default, on_mount: [{LiveBeatsWeb.UserAuth, :current_user}, LiveBeatsWeb.Nav] do
|
||||
live "/signin", SignInLive, :index
|
||||
end
|
||||
|
||||
live_session :authenticated, on_mount: [{LiveBeatsWeb.UserAuth, :ensure_authenticated}, LiveBeatsWeb.Nav] do
|
||||
live "/", HomeLive, :index
|
||||
live "/signin", SigninLive, :index
|
||||
delete "/signout", OAuthCallbackController, :sign_out
|
||||
live "/songs", SongLive.Index, :index
|
||||
live "/songs/new", SongLive.Index, :new
|
||||
live "/songs/:id/edit", SongLive.Index, :edit
|
||||
live "/songs/:id", SongLive.Show, :show
|
||||
live "/songs/:id/show/edit", SongLive.Show, :edit
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
</div>
|
||||
|
||||
<% else %>
|
||||
<%= live_redirect to: Routes.signin_path(@conn, :index),
|
||||
<%= live_redirect to: Routes.sign_in_path(@conn, :index),
|
||||
class: "text-gray-600 hover:text-gray-900 hover:bg-gray-50 group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md" do %>
|
||||
|
||||
<svg class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-6 w-6"
|
||||
|
@ -123,32 +123,16 @@
|
|||
Rooms
|
||||
</h3>
|
||||
<div class="mt-1 space-y-1" role="group" aria-labelledby="mobile-teams-headline">
|
||||
|
||||
<a href="#"
|
||||
class="group flex items-center px-3 py-2 text-base leading-5 font-medium text-gray-600 rounded-md hover:text-gray-900 hover:bg-gray-50">
|
||||
<span class="w-2.5 h-2.5 mr-4 bg-indigo-500 rounded-full" aria-hidden="true"></span>
|
||||
<span class="truncate">
|
||||
Chill
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="#"
|
||||
class="group flex items-center px-3 py-2 text-base leading-5 font-medium text-gray-600 rounded-md hover:text-gray-900 hover:bg-gray-50">
|
||||
<span class="w-2.5 h-2.5 mr-4 bg-green-500 rounded-full" aria-hidden="true"></span>
|
||||
<span class="truncate">
|
||||
Pop
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="#"
|
||||
class="group flex items-center px-3 py-2 text-base leading-5 font-medium text-gray-600 rounded-md hover:text-gray-900 hover:bg-gray-50">
|
||||
<span class="w-2.5 h-2.5 mr-4 bg-yellow-500 rounded-full" aria-hidden="true"></span>
|
||||
<span class="truncate">
|
||||
Techo
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<%= for genre <- @genres do %>
|
||||
<a href="#"
|
||||
class="group flex items-center px-3 py-2 text-base leading-5 font-medium text-gray-600 rounded-md hover:text-gray-900 hover:bg-gray-50">
|
||||
<span class="w-2.5 h-2.5 mr-4 bg-indigo-500 rounded-full" aria-hidden="true"></span>
|
||||
<span class="truncate">
|
||||
<%= genre.title %>
|
||||
</span>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -246,8 +230,8 @@
|
|||
Home
|
||||
<% end %>
|
||||
|
||||
<a href="#"
|
||||
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">
|
||||
<%= 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 %>
|
||||
<svg class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
|
@ -255,10 +239,10 @@
|
|||
d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
|
||||
</svg>
|
||||
My Songs
|
||||
</a>
|
||||
<% 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 %>
|
||||
<svg class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
|
@ -277,31 +261,15 @@
|
|||
Rooms
|
||||
</h3>
|
||||
<div class="mt-1 space-y-1" role="group" aria-labelledby="desktop-teams-headline">
|
||||
|
||||
<a href="#"
|
||||
class="group flex items-center px-3 py-2 text-sm font-medium text-gray-700 rounded-md hover:text-gray-900 hover:bg-gray-50">
|
||||
<span class="w-2.5 h-2.5 mr-4 bg-indigo-500 rounded-full" aria-hidden="true"></span>
|
||||
<span class="truncate">
|
||||
Chill
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="#"
|
||||
class="group flex items-center px-3 py-2 text-sm font-medium text-gray-700 rounded-md hover:text-gray-900 hover:bg-gray-50">
|
||||
<span class="w-2.5 h-2.5 mr-4 bg-green-500 rounded-full" aria-hidden="true"></span>
|
||||
<span class="truncate">
|
||||
Pop
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="#"
|
||||
class="group flex items-center px-3 py-2 text-sm font-medium text-gray-700 rounded-md hover:text-gray-900 hover:bg-gray-50">
|
||||
<span class="w-2.5 h-2.5 mr-4 bg-yellow-500 rounded-full" aria-hidden="true"></span>
|
||||
<span class="truncate">
|
||||
Techo
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<%= for genre <- @genres do %>
|
||||
<a href="#"
|
||||
class="group flex items-center px-3 py-2 text-base leading-5 font-medium text-gray-600 rounded-md hover:text-gray-900 hover:bg-gray-50">
|
||||
<span class="w-2.5 h-2.5 mr-4 bg-indigo-500 rounded-full" aria-hidden="true"></span>
|
||||
<span class="truncate">
|
||||
<%= genre.title %>
|
||||
</span>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
@ -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
|
||||
|
|
22
priv/repo/migrations/20211027201102_create_songs.exs
Normal file
22
priv/repo/migrations/20211027201102_create_songs.exs
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
69
test/live_beats/media_library_test.exs
Normal file
69
test/live_beats/media_library_test.exs
Normal file
|
@ -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
|
110
test/live_beats_web/live/song_live_test.exs
Normal file
110
test/live_beats_web/live/song_live_test.exs
Normal file
|
@ -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
|
25
test/support/fixtures/media_library_fixtures.ex
Normal file
25
test/support/fixtures/media_library_fixtures.ex
Normal file
|
@ -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
|
Loading…
Reference in a new issue