This commit is contained in:
Chris McCord 2022-08-03 09:40:11 -04:00
parent 51971a28a8
commit 6b02cfc614
35 changed files with 801 additions and 421 deletions

View file

@ -263,21 +263,6 @@ let Focus = {
}, },
} }
// Accessible focus wrapping
Hooks.FocusWrap = {
mounted(){
this.content = document.querySelector(this.el.getAttribute("data-content"))
this.focusStart = this.el.querySelector(`#${this.el.id}-start`)
this.focusEnd = this.el.querySelector(`#${this.el.id}-end`)
this.focusStart.addEventListener("focus", () => Focus.focusLastDescendant(this.content))
this.focusEnd.addEventListener("focus", () => Focus.focusFirstDescendant(this.content))
this.content.addEventListener("phx:show-end", () => this.content.focus())
if(window.getComputedStyle(this.content).display !== "none"){
Focus.focusFirstDescendant(this.content)
}
},
}
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, { let liveSocket = new LiveSocket("/live", Socket, {
hooks: Hooks, hooks: Hooks,
@ -297,7 +282,7 @@ let routeUpdated = () => {
// Show progress bar on live navigation and form submits // Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "rgba(147, 51, 234, 1)"}, shadowColor: "rgba(0, 0, 0, .3)"}) topbar.config({barColors: {0: "rgba(147, 51, 234, 1)"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", info => topbar.show()) window.addEventListener("phx:page-loading-start", info => topbar.delayedShow(200))
window.addEventListener("phx:page-loading-stop", info => topbar.hide()) window.addEventListener("phx:page-loading-stop", info => topbar.hide())
// Accessible routing // Accessible routing
@ -333,5 +318,4 @@ liveSocket.connect()
// >> liveSocket.enableDebug() // >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim() // >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket window.liveSocket = liveSocket

1
assets/js/phoenix Symbolic link
View file

@ -0,0 +1 @@
/Users/chris/oss/phoenix/assets/js/phoenix

1
assets/js/phoenix_live_view Symbolic link
View file

@ -0,0 +1 @@
/Users/chris/oss/phoenix_live_view/assets/js/phoenix_live_view

View file

@ -4,7 +4,7 @@
* http://buunguyen.github.io/topbar * http://buunguyen.github.io/topbar
* Copyright (c) 2021 Buu Nguyen * Copyright (c) 2021 Buu Nguyen
*/ */
(function (window, document) { (function (window, document) {
"use strict"; "use strict";
// https://gist.github.com/paulirish/1579671 // https://gist.github.com/paulirish/1579671
@ -35,10 +35,11 @@
})(); })();
var canvas, var canvas,
progressTimerId,
fadeTimerId,
currentProgress, currentProgress,
showing, showing,
progressTimerId = null,
fadeTimerId = null,
delayTimerId = null,
addEvent = function (elem, type, handler) { addEvent = function (elem, type, handler) {
if (elem.addEventListener) elem.addEventListener(type, handler, false); if (elem.addEventListener) elem.addEventListener(type, handler, false);
else if (elem.attachEvent) elem.attachEvent("on" + type, handler); else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
@ -95,6 +96,11 @@
for (var key in opts) for (var key in opts)
if (options.hasOwnProperty(key)) options[key] = opts[key]; if (options.hasOwnProperty(key)) options[key] = opts[key];
}, },
delayedShow: function(time) {
if (showing) return;
if (delayTimerId) return;
delayTimerId = setTimeout(() => topbar.show(), time);
},
show: function () { show: function () {
if (showing) return; if (showing) return;
showing = true; showing = true;
@ -125,6 +131,8 @@
return currentProgress; return currentProgress;
}, },
hide: function () { hide: function () {
clearTimeout(delayTimerId);
delayTimerId = null;
if (!showing) return; if (!showing) return;
showing = false; showing = false;
if (progressTimerId != null) { if (progressTimerId != null) {

View file

@ -11,9 +11,7 @@ config :live_beats,
replica: LiveBeats.ReplicaRepo, replica: LiveBeats.ReplicaRepo,
ecto_repos: [LiveBeats.Repo] ecto_repos: [LiveBeats.Repo]
config :live_beats, :files, admin_usernames: []
config :live_beats, :files,
admin_usernames: []
# Configures the endpoint # Configures the endpoint
config :live_beats, LiveBeatsWeb.Endpoint, config :live_beats, LiveBeatsWeb.Endpoint,

View file

@ -19,8 +19,7 @@ if config_env() == :prod do
For example: ecto://USER:PASS@HOST/DATABASE For example: ecto://USER:PASS@HOST/DATABASE
""" """
replica_database_url = replica_database_url = System.get_env("REPLICA_DATABASE_URL") || database_url
System.get_env("REPLICA_DATABASE_URL") || database_url
host = System.get_env("PHX_HOST") || "example.com" host = System.get_env("PHX_HOST") || "example.com"
ecto_ipv6? = System.get_env("ECTO_IPV6") == "true" ecto_ipv6? = System.get_env("ECTO_IPV6") == "true"
@ -69,7 +68,6 @@ if config_env() == :prod do
hostname: "livebeats.local", hostname: "livebeats.local",
transport_opts: [inet6: true] transport_opts: [inet6: true]
config :live_beats, :github, config :live_beats, :github,
client_id: System.fetch_env!("LIVE_BEATS_GITHUB_CLIENT_ID"), client_id: System.fetch_env!("LIVE_BEATS_GITHUB_CLIENT_ID"),
client_secret: System.fetch_env!("LIVE_BEATS_GITHUB_CLIENT_SECRET") client_secret: System.fetch_env!("LIVE_BEATS_GITHUB_CLIENT_SECRET")

View file

@ -30,7 +30,6 @@ config :live_beats, LiveBeats.ReplicaRepo,
pool_size: 10, pool_size: 10,
priv: "priv/repo" priv: "priv/repo"
# We don't run a server during test. If one is required, # We don't run a server during test. If one is required,
# you can enable the server option below. # you can enable the server option below.
config :live_beats, LiveBeatsWeb.Endpoint, config :live_beats, LiveBeatsWeb.Endpoint,

View file

@ -19,6 +19,7 @@ defmodule LiveBeats do
""" """
def config([main_key | rest] = keyspace) when is_list(keyspace) do def config([main_key | rest] = keyspace) when is_list(keyspace) do
main = Application.fetch_env!(:live_beats, main_key) main = Application.fetch_env!(:live_beats, main_key)
Enum.reduce(rest, main, fn next_key, current -> Enum.reduce(rest, main, fn next_key, current ->
case Keyword.fetch(current, next_key) do case Keyword.fetch(current, next_key) do
{:ok, val} -> val {:ok, val} -> val
@ -85,11 +86,11 @@ defmodule LiveBeats do
target.handle_execute({src_mod, event_struct}) target.handle_execute({src_mod, event_struct})
catch catch
kind, err -> kind, err ->
Logger.error """ Logger.error("""
executing {#{inspect(src_mod)}, #{inspect(event_mod)}} failed with #{inspect(kind)} executing {#{inspect(src_mod)}, #{inspect(event_mod)}} failed with #{inspect(kind)}
#{inspect(err)} #{inspect(err)}
""" """)
end end
end end
end end

View file

@ -31,11 +31,17 @@ defmodule LiveBeats.Accounts.Identity do
"provider_id" => to_string(info["id"]), "provider_id" => to_string(info["id"]),
"provider_login" => info["login"], "provider_login" => info["login"],
"provider_name" => info["name"] || info["login"], "provider_name" => info["name"] || info["login"],
"provider_email" => primary_email, "provider_email" => primary_email
} }
%Identity{provider: @github, provider_meta: %{"user" => info, "emails" => emails}} %Identity{provider: @github, provider_meta: %{"user" => info, "emails" => emails}}
|> cast(params, [:provider_token, :provider_email, :provider_login, :provider_name, :provider_id]) |> cast(params, [
:provider_token,
:provider_email,
:provider_login,
:provider_name,
:provider_id
])
|> validate_required([:provider_token, :provider_email, :provider_name, :provider_id]) |> validate_required([:provider_token, :provider_email, :provider_name, :provider_id])
|> validate_length(:provider_meta, max: 10_000) |> validate_length(:provider_meta, max: 10_000)
end end

View file

@ -4,7 +4,6 @@ defmodule LiveBeats.Accounts.User do
alias LiveBeats.Accounts.{User, Identity} alias LiveBeats.Accounts.{User, Identity}
schema "users" do schema "users" do
field :email, :string field :email, :string
field :name, :string field :name, :string
@ -61,7 +60,6 @@ defmodule LiveBeats.Accounts.User do
|> validate_username() |> validate_username()
end end
defp validate_email(changeset) do defp validate_email(changeset) do
changeset changeset
|> validate_required([:email]) |> validate_required([:email])

View file

@ -1,6 +1,7 @@
defmodule LiveBeats.Github do defmodule LiveBeats.Github do
def authorize_url() do def authorize_url() do
state = random_string() state = random_string()
"https://github.com/login/oauth/authorize?client_id=#{client_id()}&state=#{state}&scope=user:email" "https://github.com/login/oauth/authorize?client_id=#{client_id()}&state=#{state}&scope=user:email"
end end
@ -34,6 +35,7 @@ defmodule LiveBeats.Github do
end end
defp fetch_user_info({:error, _reason} = error), do: error defp fetch_user_info({:error, _reason} = error), do: error
defp fetch_user_info({:ok, token}) do defp fetch_user_info({:ok, token}) do
resp = resp =
http( http(
@ -43,6 +45,7 @@ defmodule LiveBeats.Github do
[], [],
[{"accept", "application/vnd.github.v3+json"}, {"Authorization", "token #{token}"}] [{"accept", "application/vnd.github.v3+json"}, {"Authorization", "token #{token}"}]
) )
case resp do case resp do
{:ok, info} -> {:ok, %{info: Jason.decode!(info), token: token}} {:ok, info} -> {:ok, %{info: Jason.decode!(info), token: token}}
{:error, _reason} = err -> err {:error, _reason} = err -> err
@ -50,6 +53,7 @@ defmodule LiveBeats.Github do
end end
defp fetch_emails({:error, _} = err), do: err defp fetch_emails({:error, _} = err), do: err
defp fetch_emails({:ok, user}) do defp fetch_emails({:ok, user}) do
resp = resp =
http( http(
@ -59,6 +63,7 @@ defmodule LiveBeats.Github do
[], [],
[{"accept", "application/vnd.github.v3+json"}, {"Authorization", "token #{user.token}"}] [{"accept", "application/vnd.github.v3+json"}, {"Authorization", "token #{user.token}"}]
) )
case resp do case resp do
{:ok, info} -> {:ok, info} ->
emails = Jason.decode!(info) emails = Jason.decode!(info)

View file

@ -16,6 +16,7 @@ defmodule LiveBeats.MediaLibrary.Genre do
end end
defp put_slug(%Ecto.Changeset{valid?: false} = changeset), do: changeset defp put_slug(%Ecto.Changeset{valid?: false} = changeset), do: changeset
defp put_slug(%Ecto.Changeset{valid?: true} = changeset) do defp put_slug(%Ecto.Changeset{valid?: true} = changeset) do
if title = get_change(changeset, :title) do if title = get_change(changeset, :title) do
put_change(changeset, :slug, Phoenix.Naming.underscore(title)) put_change(changeset, :slug, Phoenix.Naming.underscore(title))

View file

@ -90,8 +90,11 @@ defmodule LiveBeatsWeb.Presence do
|> assign_new(:total_count, fn -> count end) |> assign_new(:total_count, fn -> count end)
~H""" ~H"""
<div class="px-4 mt-6 sm:px-6 lg:px-8"> <!-- users --> <div class="px-4 mt-6 sm:px-6 lg:px-8">
<h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide">Listening now (<%= @count %>)</h2> <!-- users -->
<h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide">
Listening now (<%= @count %>)
</h2>
<ul <ul
id="listening-now" id="listening-now"
role="list" role="list"
@ -99,11 +102,7 @@ defmodule LiveBeatsWeb.Presence do
class="grid grid-cols-1 gap-4 sm:gap-4 sm:grid-cols-2 xl:grid-cols-5 mt-3" class="grid grid-cols-1 gap-4 sm:gap-4 sm:grid-cols-2 xl:grid-cols-5 mt-3"
> >
<%= for {id, _time} <- Enum.sort(@presence_ids, fn {_, t1}, {_, t2} -> t1 < t2 end) do %> <%= for {id, _time} <- Enum.sort(@presence_ids, fn {_, t1}, {_, t2} -> t1 < t2 end) do %>
<.live_component <.live_component id={id} module={BadgeComponent} presence={@presences[id]} />
id={id}
module={BadgeComponent}
presence={@presences[id]}
/>
<% end %> <% end %>
</ul> </ul>
<%= if @total_count > @count do %> <%= if @total_count > @count do %>
@ -125,7 +124,7 @@ end
defmodule LiveBeatsWeb.Presence.BadgeComponent do defmodule LiveBeatsWeb.Presence.BadgeComponent do
use LiveBeatsWeb, :live_component use LiveBeatsWeb, :live_component
# https://fly.io/docs/reference/regions/ #  https://fly.io/docs/reference/regions/
@region_names %{ @region_names %{
"ams" => "Amsterdam, Netherlands", "ams" => "Amsterdam, Netherlands",
"atl" => "Atlanta, Georgia (US)", "atl" => "Atlanta, Georgia (US)",
@ -152,14 +151,27 @@ defmodule LiveBeatsWeb.Presence.BadgeComponent do
def render(assigns) do def render(assigns) do
~H""" ~H"""
<li id={"presence-#{@id}"} class="relative col-span-1 flex shadow-sm rounded-md overflow-hidden"> <li id={"presence-#{@id}"} class="relative col-span-1 flex shadow-sm rounded-md overflow-hidden">
<.link navigate={profile_path(@presence)} class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate"> <.link
<img class="w-12 h-12 flex-shrink-0 flex items-center justify-center rounded-l-md bg-purple-600" src={@presence.avatar_url} alt=""> navigate={profile_path(@presence)}
class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate"
>
<img
class="w-12 h-12 flex-shrink-0 flex items-center justify-center rounded-l-md bg-purple-600"
src={@presence.avatar_url}
alt=""
/>
<div class="flex-1 flex items-center justify-between text-gray-900 text-sm font-medium hover:text-gray-600 pl-3"> <div class="flex-1 flex items-center justify-between text-gray-900 text-sm font-medium hover:text-gray-600 pl-3">
<div class="flex-1 py-1 text-sm truncate"> <div class="flex-1 py-1 text-sm truncate">
<%= @presence.username %> <%= @presence.username %>
<%= if @ping do %> <%= if @ping do %>
<p class="text-gray-400 text-xs">ping: <%= @ping %>ms</p> <p class="text-gray-400 text-xs">ping: <%= @ping %>ms</p>
<%= if @region do %><img class="inline w-7 h-7 absolute right-3 top-3" src={"https://fly.io/ui/images/#{@region}.svg"} title={region_name(@region)} /><% end %> <%= if @region do %>
<img
class="inline w-7 h-7 absolute right-3 top-3"
src={"https://fly.io/ui/images/#{@region}.svg"}
title={region_name(@region)}
/>
<% end %>
<% end %> <% end %>
</div> </div>
</div> </div>

View file

@ -10,7 +10,6 @@ defmodule LiveBeatsWeb.OAuthCallbackController do
with {:ok, info} <- client.exchange_access_token(code: code, state: state), with {:ok, info} <- client.exchange_access_token(code: code, state: state),
%{info: info, primary_email: primary, emails: emails, token: token} = info, %{info: info, primary_email: primary, emails: emails, token: token} = info,
{:ok, user} <- Accounts.register_github_user(primary, info, emails, token) do {:ok, user} <- Accounts.register_github_user(primary, info, emails, token) do
conn conn
|> put_flash(:info, "Welcome #{user.email}") |> put_flash(:info, "Welcome #{user.email}")
|> LiveBeatsWeb.UserAuth.log_in_user(user) |> LiveBeatsWeb.UserAuth.log_in_user(user)
@ -19,7 +18,10 @@ defmodule LiveBeatsWeb.OAuthCallbackController do
Logger.debug("failed GitHub insert #{inspect(changeset.errors)}") Logger.debug("failed GitHub insert #{inspect(changeset.errors)}")
conn conn
|> put_flash(:error, "We were unable to fetch the necessary information from your GithHub account") |> put_flash(
:error,
"We were unable to fetch the necessary information from your GithHub account"
)
|> redirect(to: "/") |> redirect(to: "/")
{:error, reason} -> {:error, reason} ->
@ -40,6 +42,6 @@ defmodule LiveBeatsWeb.OAuthCallbackController do
end end
defp github_client(conn) do defp github_client(conn) do
conn.assigns[:github_client] || LiveBeats.Github conn.assigns[:github_client] || LiveBeats.Github
end end
end end

View file

@ -19,7 +19,9 @@ defmodule LiveBeatsWeb.UserAuth do
def on_mount(:ensure_authenticated, _params, session, socket) do def on_mount(:ensure_authenticated, _params, session, socket) do
case session do case session do
%{"user_id" => user_id} -> %{"user_id" => user_id} ->
new_socket = LiveView.assign_new(socket, :current_user, fn -> Accounts.get_user!(user_id) end) new_socket =
LiveView.assign_new(socket, :current_user, fn -> Accounts.get_user!(user_id) end)
%Accounts.User{} = new_socket.assigns.current_user %Accounts.User{} = new_socket.assigns.current_user
{:cont, new_socket} {:cont, new_socket}

View file

@ -158,42 +158,6 @@ defmodule LiveBeatsWeb.LiveHelpers do
""" """
end end
attr :navigate, :string
attr :patch, :string
attr :href, :string, default: nil
attr :replace, :string, default: false
attr :rest, :global
def link(%{navigate: _to} = assigns) do
assigns = assign_new(assigns, :class, fn -> nil end)
~H"""
<a href={@navigate} data-phx-link="redirect" data-phx-link-state="push" {@rest}>
<%= render_slot(@inner_block) %>
</a>
"""
end
def link(%{patch: _to} = assigns) do
~H"""
<a
href={@patch}
data-phx-link="patch"
data-phx-link-state={if @replace, do: "replace", else: "push"}
{@rest}
>
<%= render_slot(@inner_block) %>
</a>
"""
end
def link(%{} = assigns) do
~H"""
<a href={@href || "#"} {@rest}>
<%= render_slot(@inner_block) %>
</a>
"""
end
@doc """ @doc """
Returns a button triggered dropdown with aria keyboard and focus supporrt. Returns a button triggered dropdown with aria keyboard and focus supporrt.
@ -487,19 +451,6 @@ defmodule LiveBeatsWeb.LiveHelpers do
""" """
end end
attr :id, :string, required: true
attr :content, :string
def focus_wrap(assigns) do
~H"""
<div id={@id} phx-hook="FocusWrap" data-content={@content}>
<span id={"#{@id}-start"} tabindex="0" aria-hidden="true"></span>
<%= render_slot(@inner_block) %>
<span id={"#{@id}-end"} tabindex="0" aria-hidden="true"></span>
</div>
"""
end
attr :id, :string, required: true attr :id, :string, required: true
attr :min, :integer, default: 0 attr :min, :integer, default: 0
attr :max, :integer, default: 100 attr :max, :integer, default: 100

View file

@ -29,9 +29,11 @@ defmodule LiveBeatsWeb.PlayerLive do
<.progress_bar id="player-progress" /> <.progress_bar id="player-progress" />
<div id="player-info" <div
id="player-info"
class="text-gray-500 dark:text-gray-400 flex-row justify-between text-sm font-medium tabular-nums" class="text-gray-500 dark:text-gray-400 flex-row justify-between text-sm font-medium tabular-nums"
phx-update="ignore"> phx-update="ignore"
>
<div id="player-time"></div> <div id="player-time"></div>
<div id="player-duration"></div> <div id="player-duration"></div>
</div> </div>
@ -43,7 +45,7 @@ defmodule LiveBeatsWeb.PlayerLive do
navigate={profile_path(@profile)} navigate={profile_path(@profile)}
class="mx-auto flex border-2 border-white border-opacity-20 rounded-md p-1 pr-2" class="mx-auto flex border-2 border-white border-opacity-20 rounded-md p-1 pr-2"
> >
<span class="mt-1"><.icon name={:user_circle} class="w-4 h-4 block"/></span> <span class="mt-1"><.icon name={:user_circle} class="w-4 h-4 block" /></span>
<p class="ml-2"><%= @profile.username %></p> <p class="ml-2"><%= @profile.username %></p>
</.link> </.link>
<% else %> <% else %>
@ -52,7 +54,12 @@ defmodule LiveBeatsWeb.PlayerLive do
<%= if is_nil(@profile) or @own_profile? do %> <%= if is_nil(@profile) or @own_profile? do %>
<!-- prev --> <!-- prev -->
<button type="button" class="sm:block xl:block mx-auto scale-75" phx-click={js_prev(@own_profile?)} aria-label="Previous"> <button
type="button"
class="sm:block xl:block mx-auto scale-75"
phx-click={js_prev(@own_profile?)}
aria-label="Previous"
>
<svg width="17" height="18"> <svg width="17" height="18">
<path d="M0 0h2v18H0V0zM4 9l13-9v18L4 9z" fill="currentColor" /> <path d="M0 0h2v18H0V0zM4 9l13-9v18L4 9z" fill="currentColor" />
</svg> </svg>
@ -60,23 +67,70 @@ defmodule LiveBeatsWeb.PlayerLive do
<!-- /prev --> <!-- /prev -->
<!-- play/pause --> <!-- play/pause -->
<button type="button" class="mx-auto scale-75" phx-click={js_play_pause(@own_profile?)} aria-label={if @playing do "Pause" else "Play" end}> <button
type="button"
class="mx-auto scale-75"
phx-click={js_play_pause(@own_profile?)}
aria-label={
if @playing do
"Pause"
else
"Play"
end
}
>
<%= if @playing do %> <%= if @playing do %>
<svg id="player-pause" width="50" height="50" fill="none"> <svg id="player-pause" width="50" height="50" fill="none">
<circle class="text-gray-300 dark:text-gray-500" cx="25" cy="25" r="24" stroke="currentColor" stroke-width="1.5" /> <circle
class="text-gray-300 dark:text-gray-500"
cx="25"
cy="25"
r="24"
stroke="currentColor"
stroke-width="1.5"
/>
<path d="M18 16h4v18h-4V16zM28 16h4v18h-4z" fill="currentColor" /> <path d="M18 16h4v18h-4V16zM28 16h4v18h-4z" fill="currentColor" />
</svg> </svg>
<% else %> <% else %>
<svg id="player-play" width="50" height="50" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<circle id="svg_1" stroke-width="0.8" stroke="currentColor" r="11.4" cy="12" cx="12" class="text-gray-300 dark:text-gray-500"/> id="player-play"
<path stroke="null" fill="currentColor" transform="rotate(90 12.8947 12.3097)" id="svg_6" d="m9.40275,15.10014l3.49194,-5.58088l3.49197,5.58088l-6.98391,0z" stroke-width="1.5" fill="none"/> width="50"
height="50"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<circle
id="svg_1"
stroke-width="0.8"
stroke="currentColor"
r="11.4"
cy="12"
cx="12"
class="text-gray-300 dark:text-gray-500"
/>
<path
stroke="null"
fill="currentColor"
transform="rotate(90 12.8947 12.3097)"
id="svg_6"
d="m9.40275,15.10014l3.49194,-5.58088l3.49197,5.58088l-6.98391,0z"
stroke-width="1.5"
fill="none"
/>
</svg> </svg>
<% end %> <% end %>
</button> </button>
<!-- /play/pause --> <!-- /play/pause -->
<!-- next --> <!-- next -->
<button type="button" class="mx-auto scale-75" phx-click={js_next(@own_profile?)} aria-label="Next"> <button
type="button"
class="mx-auto scale-75"
phx-click={js_next(@own_profile?)}
aria-label="Next"
>
<svg width="17" height="18" viewBox="0 0 17 18" fill="none"> <svg width="17" height="18" viewBox="0 0 17 18" fill="none">
<path d="M17 0H15V18H17V0Z" fill="currentColor" /> <path d="M17 0H15V18H17V0Z" fill="currentColor" />
<path d="M13 9L0 0V18L13 9Z" fill="currentColor" /> <path d="M13 9L0 0V18L13 9Z" fill="currentColor" />
@ -86,8 +140,14 @@ defmodule LiveBeatsWeb.PlayerLive do
<% else %> <% else %>
<button type="button" class="mx-auto scale-75"></button> <button type="button" class="mx-auto scale-75"></button>
<!-- stop button --> <!-- stop button -->
<button type="button" class="mx-auto scale-75" phx-click={JS.push("switch_profile", value: %{user_id: nil}, target: "#player", loading: "#player")}> <button
<.icon name={:stop} class="h-12 w-12"/> type="button"
class="mx-auto scale-75"
phx-click={
JS.push("switch_profile", value: %{user_id: nil}, target: "#player", loading: "#player")
}
>
<.icon name={:stop} class="h-12 w-12" />
</button> </button>
<!-- stop button --> <!-- stop button -->
<% end %> <% end %>
@ -106,9 +166,7 @@ defmodule LiveBeatsWeb.PlayerLive do
<%= if @profile do %> <%= if @profile do %>
<.modal id="not-authorized" on_confirm={hide_modal("not-authorized")}> <.modal id="not-authorized" on_confirm={hide_modal("not-authorized")}>
<:title>You can't do that</:title> <:title>You can't do that</:title>
Only <%= @profile.username %> can control playback Only <%= @profile.username %> can control playback
<:confirm>Ok</:confirm> <:confirm>Ok</:confirm>
</.modal> </.modal>
<% end %> <% end %>
@ -156,10 +214,11 @@ defmodule LiveBeatsWeb.PlayerLive do
if profile && connected?(socket) do if profile && connected?(socket) do
current_user = Accounts.update_active_profile(current_user, profile.user_id) current_user = Accounts.update_active_profile(current_user, profile.user_id)
#untrack last profile the user was listening # untrack last profile the user was listening
if socket.assigns.profile do if socket.assigns.profile do
Presence.untrack_profile_user(socket.assigns.profile, current_user.id) Presence.untrack_profile_user(socket.assigns.profile, current_user.id)
end end
Presence.track_profile_user(profile, current_user.id) Presence.track_profile_user(profile, current_user.id)
send(self(), :play_current) send(self(), :play_current)
@ -311,7 +370,7 @@ defmodule LiveBeatsWeb.PlayerLive do
vsn: 1, vsn: 1,
ip: to_string(song.server_ip), ip: to_string(song.server_ip),
size: song.mp3_filesize, size: song.mp3_filesize,
uuid: song.mp3_filename, uuid: song.mp3_filename
}) })
push_event(socket, "play", %{ push_event(socket, "play", %{

View file

@ -12,30 +12,42 @@ defmodule LiveBeatsWeb.ProfileLive do
<.title_bar> <.title_bar>
<div> <div>
<div class="block"> <div class="block">
<%= @profile.tagline %> <%= if @owns_profile? do %>(you)<% end %> <%= @profile.tagline %>
<%= if @owns_profile? do %>
(you)
<% end %>
</div> </div>
<.link href={@profile.external_homepage_url} target="_blank" class="text-sm text-gray-600"> <.link href={@profile.external_homepage_url} target="_blank" class="text-sm text-gray-600">
<.icon name={:code}/> <span class=""><%= url_text(@profile.external_homepage_url) %></span> <.icon name={:code} /> <span class=""><%= url_text(@profile.external_homepage_url) %></span>
</.link> </.link>
</div> </div>
<:actions> <:actions>
<%= if @active_profile_id == @profile.user_id do %> <%= if @active_profile_id == @profile.user_id do %>
<.button primary <.button
phx-click={JS.push("switch_profile", value: %{user_id: nil}, target: "#player", loading: "#player")} primary
phx-click={
JS.push("switch_profile", value: %{user_id: nil}, target: "#player", loading: "#player")
}
> >
<.icon name={:stop}/><span class="ml-2">Stop Listening</span> <.icon name={:stop} /><span class="ml-2">Stop Listening</span>
</.button> </.button>
<% else %> <% else %>
<.button primary <.button
phx-click={JS.push("switch_profile", value: %{user_id: @profile.user_id}, target: "#player", loading: "#player")} primary
phx-click={
JS.push("switch_profile",
value: %{user_id: @profile.user_id},
target: "#player",
loading: "#player"
)
}
> >
<.icon name={:play}/><span class="ml-2">Listen</span> <.icon name={:play} /><span class="ml-2">Listen</span>
</.button> </.button>
<% end %> <% end %>
<%= if @owns_profile? do %> <%= if @owns_profile? do %>
<.button id="upload-btn" primary patch={profile_path(@current_user, :new)}> <.button id="upload-btn" primary patch={profile_path(@current_user, :new)}>
<.icon name={:upload}/><span class="ml-2">Upload Songs</span> <.icon name={:upload} /><span class="ml-2">Upload Songs</span>
</.button> </.button>
<% end %> <% end %>
</:actions> </:actions>
@ -73,18 +85,23 @@ defmodule LiveBeatsWeb.ProfileLive do
row_id={fn song -> "song-#{song.id}" end} row_id={fn song -> "song-#{song.id}" end}
owns_profile?={@owns_profile?} owns_profile?={@owns_profile?}
> >
<:col let={%{song: song}} label="Title"><%= song.title %></:col> <:col :let={%{song: song}} label="Title"><%= song.title %></:col>
<:col let={%{song: song}} label="Artist"><%= song.artist %></:col> <:col :let={%{song: song}} label="Artist"><%= song.artist %></:col>
<:col let={%{song: song}} label="Attribution" class="max-w-5xl break-words text-gray-600 font-light"><%= song.attribution %></:col> <:col
<:col let={%{song: song}} label="Duration"><%= MP3Stat.to_mmss(song.duration) %></:col> :let={%{song: song}}
<:col let={%{song: song}} label="" if={@owns_profile?}> label="Attribution"
class="max-w-5xl break-words text-gray-600 font-light"
>
<%= song.attribution %>
</:col>
<:col :let={%{song: song}} label="Duration"><%= MP3Stat.to_mmss(song.duration) %></:col>
<:col :let={%{song: song}} label="" if={@owns_profile?}>
<.link <.link
id={"delete-song-#{song.id}"} id={"delete-song-#{song.id}"}
phx-click={show_modal("delete-modal-#{song.id}")} phx-click={show_modal("delete-modal-#{song.id}")}
class="inline-flex items-center px-3 py-2 text-sm leading-4 font-medium" class="inline-flex items-center px-3 py-2 text-sm leading-4 font-medium"
> >
<.icon name={:trash} class="-ml-0.5 mr-2 h-4 w-4"/> <.icon name={:trash} class="-ml-0.5 mr-2 h-4 w-4" /> Delete
Delete
</.link> </.link>
</:col> </:col>
</.live_table> </.live_table>

View file

@ -17,21 +17,31 @@ defmodule LiveBeatsWeb.ProfileLive.SongEntryComponent do
<% else %> <% else %>
Title Title
<span class="text-gray-400"> <span class="text-gray-400">
(calculating duration <.spinner class="inline-block animate-spin h-2.5 w-2.5 text-gray-400"/>) (calculating duration
<.spinner class="inline-block animate-spin h-2.5 w-2.5 text-gray-400" />)
</span> </span>
<% end %> <% end %>
</label> </label>
<input type="text" name={"songs[#{@ref}][title]"} value={@title} <input
class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm" {%{autofocus: @index == 0}}/> type="text"
name={"songs[#{@ref}][title]"}
value={@title}
class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm"
{%{autofocus: @index == 0}}
/>
</div> </div>
<div class="border border-gray-300 rounded-md px-3 py-2 mt-2 shadow-sm focus-within:ring-1 focus-within:ring-indigo-600 focus-within:border-indigo-600"> <div class="border border-gray-300 rounded-md px-3 py-2 mt-2 shadow-sm focus-within:ring-1 focus-within:ring-indigo-600 focus-within:border-indigo-600">
<label for="name" class="block text-xs font-medium text-gray-900">Artist</label> <label for="name" class="block text-xs font-medium text-gray-900">Artist</label>
<input type="text" name={"songs[#{@ref}][artist]"} value={@artist} <input
class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm"/> type="text"
name={"songs[#{@ref}][artist]"}
value={@artist}
class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm"
/>
</div> </div>
<div class="col-span-full sm:grid sm:grid-cols-2 sm:gap-2 sm:items-start"> <div class="col-span-full sm:grid sm:grid-cols-2 sm:gap-2 sm:items-start">
<.error input_name={"songs[#{@ref}][title]"} field={:title} errors={@errors} class="-mt-1"/> <.error input_name={"songs[#{@ref}][title]"} field={:title} errors={@errors} class="-mt-1" />
<.error input_name={"songs[#{@ref}][artist]"} field={:artist} errors={@errors} class="-mt-1"/> <.error input_name={"songs[#{@ref}][artist]"} field={:artist} errors={@errors} class="-mt-1" />
</div> </div>
<div class="border col-span-full border-gray-300 rounded-md px-3 py-2 mt-2 shadow-sm focus-within:ring-1 focus-within:ring-indigo-600 focus-within:border-indigo-600"> <div class="border col-span-full border-gray-300 rounded-md px-3 py-2 mt-2 shadow-sm focus-within:ring-1 focus-within:ring-indigo-600 focus-within:border-indigo-600">
<label for="name" class="block text-xs font-medium text-gray-900"> <label for="name" class="block text-xs font-medium text-gray-900">
@ -43,7 +53,12 @@ defmodule LiveBeatsWeb.ProfileLive.SongEntryComponent do
><%= @attribution %></textarea> ><%= @attribution %></textarea>
</div> </div>
<div class="col-span-full sm:grid sm:grid-cols-2 sm:gap-2 sm:items-start"> <div class="col-span-full sm:grid sm:grid-cols-2 sm:gap-2 sm:items-start">
<.error input_name={"songs[#{@ref}][attribution]"} field={:attribution} errors={@errors} class="-mt-1"/> <.error
input_name={"songs[#{@ref}][attribution]"}
field={:attribution}
errors={@errors}
class="-mt-1"
/>
</div> </div>
<div <div
role="progressbar" role="progressbar"
@ -52,7 +67,8 @@ defmodule LiveBeatsWeb.ProfileLive.SongEntryComponent do
aria-valuenow={@progress} aria-valuenow={@progress}
style={"transition: width 0.5s ease-in-out; width: #{@progress}%; min-width: 1px;"} style={"transition: width 0.5s ease-in-out; width: #{@progress}%; min-width: 1px;"}
class="col-span-full bg-purple-500 dark:bg-purple-400 h-1.5 w-0 p-0" class="col-span-full bg-purple-500 dark:bg-purple-400 h-1.5 w-0 p-0"
></div> >
</div>
</div> </div>
""" """
end end

View file

@ -10,27 +10,39 @@ defmodule LiveBeatsWeb.ProfileLive.SongRowComponent do
<tr id={@id} class={@class} tabindex="0"> <tr id={@id} class={@class} tabindex="0">
<%= for {col, i} <- Enum.with_index(@col) do %> <%= for {col, i} <- Enum.with_index(@col) do %>
<td <td
class={"px-6 py-3 text-sm font-medium text-gray-900 #{if i == 0, do: "w-80 cursor-pointer"} #{col[:class]}"} class={
"px-6 py-3 text-sm font-medium text-gray-900 #{if i == 0, do: "w-80 cursor-pointer"} #{col[:class]}"
}
phx-click={JS.push("play_or_pause", value: %{id: @song.id})} phx-click={JS.push("play_or_pause", value: %{id: @song.id})}
> >
<div class="flex items-center space-x-3 lg:pl-2"> <div class="flex items-center space-x-3 lg:pl-2">
<%= if i == 0 do %> <%= if i == 0 do %>
<%= if @status == :playing do %> <%= if @status == :playing do %>
<span class="flex pt-1 relative mr-2 w-4"> <span class="flex pt-1 relative mr-2 w-4">
<span class="w-3 h-3 animate-ping bg-purple-400 rounded-full absolute"></span> <span class="w-3 h-3 animate-ping bg-purple-400 rounded-full absolute"></span>
<.icon name={:volume_up} class="h-5 w-5 -mt-1 -ml-1" aria-label="Playing" role="button"/> <.icon
name={:volume_up}
class="h-5 w-5 -mt-1 -ml-1"
aria-label="Playing"
role="button"
/>
</span> </span>
<% end %> <% end %>
<%= if @status == :paused do %> <%= if @status == :paused do %>
<span class="flex pt-1 relative mr-2 w-4"> <span class="flex pt-1 relative mr-2 w-4">
<.icon name={:volume_up} class="h-5 w-5 -mt-1 -ml-1 text-gray-400" aria-label="Paused" role="button"/> <.icon
name={:volume_up}
class="h-5 w-5 -mt-1 -ml-1 text-gray-400"
aria-label="Paused"
role="button"
/>
</span> </span>
<% end %> <% end %>
<%= if @status == :stopped do %> <%= if @status == :stopped do %>
<span class="flex relative w-6 -translate-x-1"> <span class="flex relative w-6 -translate-x-1">
<%= if @owns_profile? do %> <%= if @owns_profile? do %>
<.icon name={:play} class="h-5 w-5 text-gray-400" aria-label="Play" role="button"/> <.icon name={:play} class="h-5 w-5 text-gray-400" aria-label="Play" role="button" />
<% end %> <% end %>
</span> </span>
<% end %> <% end %>
<% end %> <% end %>
@ -42,7 +54,8 @@ defmodule LiveBeatsWeb.ProfileLive.SongRowComponent do
""" """
end end
def update(%{action: :send, status: status}, socket) when status in [:playing, :paused, :stopped] do def update(%{action: :send, status: status}, socket)
when status in [:playing, :paused, :stopped] do
{:ok, assign(socket, status: status)} {:ok, assign(socket, status: status)}
end end

View file

@ -169,7 +169,7 @@ defmodule LiveBeatsWeb.ProfileLive.UploadFormComponent do
do: ~H|Something went wrong| do: ~H|Something went wrong|
defp file_error(%{kind: %Ecto.Changeset{}} = assigns), defp file_error(%{kind: %Ecto.Changeset{}} = assigns),
do: ~H|<%= @label %>: <%= LiveBeatsWeb.ErrorHelpers.translate_changeset_errors(@kind) %>| do: ~H|<%= @label %>: <%= LiveBeatsWeb.ErrorHelpers.translate_changeset_errors(@kind) %>|
defp file_error(%{kind: {msg, opts}} = assigns) when is_binary(msg) and is_list(opts), defp file_error(%{kind: {msg, opts}} = assigns) when is_binary(msg) and is_list(opts),
do: ~H|<%= @label %>: <%= LiveBeatsWeb.ErrorHelpers.translate_error(@kind) %>| do: ~H|<%= @label %>: <%= LiveBeatsWeb.ErrorHelpers.translate_error(@kind) %>|

View file

@ -7,14 +7,13 @@
class="space-y-8" class="space-y-8"
phx-target={@myself} phx-target={@myself}
phx-change="validate" phx-change="validate"
phx-submit="save"> phx-submit="save"
>
<div class="space-y-8 divide-y divide-gray-200 sm:space-y-5"> <div class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
<div class="space-y-2 sm:space-y-2"> <div class="space-y-2 sm:space-y-2">
<%= for {{ref, changeset}, i} <- Enum.with_index(@changesets) do %> <%= for {{ref, changeset}, i} <- Enum.with_index(@changesets) do %>
<.live_component id={ref} module={SongEntryComponent} changeset={changeset} index={i} /> <.live_component id={ref} module={SongEntryComponent} changeset={changeset} index={i} />
<% end %> <% end %>
<!-- upload --> <!-- upload -->
<div class="sm:grid sm:border-t sm:border-gray-200 sm:pt-5"> <div class="sm:grid sm:border-t sm:border-gray-200 sm:pt-5">
<div class="mt-1 sm:mt-0" phx-drop-target={@uploads.mp3.ref}> <div class="mt-1 sm:mt-0" phx-drop-target={@uploads.mp3.ref}>
@ -22,7 +21,7 @@
<div class="rounded-md bg-red-50 p-4 mb-2"> <div class="rounded-md bg-red-50 p-4 mb-2">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<.icon name={:x_circle} class="h-5 w-5 text-red-400"/> <.icon name={:x_circle} class="h-5 w-5 text-red-400" />
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-red-800"> <h3 class="text-sm font-medium text-red-800">
@ -42,13 +41,30 @@
<div class="max-w-lg flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md"> <div class="max-w-lg flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div class="space-y-1 text-center"> <div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true"> <svg
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> class="mx-auto h-12 w-12 text-gray-400"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
</path>
</svg> </svg>
<div class="flex text-sm text-gray-600"> <div class="flex text-sm text-gray-600">
<label for="file-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500"> <label
<span phx-click={js_exec("##{@uploads.mp3.ref}", "click", [])}>Upload files</span> for="file-upload"
<%= live_file_input @uploads.mp3, class: "sr-only", tabindex: "0" %> class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500"
>
<span phx-click={js_exec("##{@uploads.mp3.ref}", "click", [])}>
Upload files
</span>
<%= live_file_input(@uploads.mp3, class: "sr-only", tabindex: "0") %>
</label> </label>
<p class="pl-1">or drag and drop</p> <p class="pl-1">or drag and drop</p>
</div> </div>

View file

@ -10,7 +10,13 @@ defmodule LiveBeatsWeb.SettingsLive do
</.title_bar> </.title_bar>
<div class="max-w-3xl px-4 mx-auto mt-6"> <div class="max-w-3xl px-4 mx-auto mt-6">
<.form let={f} for={@changeset} phx-change="validate" phx-submit="save" class="space-y-8 divide-y divide-gray-200"> <.form
:let={f}
for={@changeset}
phx-change="validate"
phx-submit="save"
class="space-y-8 divide-y divide-gray-200"
>
<div class="space-y-8 divide-y divide-gray-200"> <div class="space-y-8 divide-y divide-gray-200">
<div> <div>
<div> <div>
@ -28,8 +34,16 @@ defmodule LiveBeatsWeb.SettingsLive do
<span class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm"> <span class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm">
<%= URI.parse(LiveBeatsWeb.Endpoint.url()).host %>/ <%= URI.parse(LiveBeatsWeb.Endpoint.url()).host %>/
</span> </span>
<%= text_input f, :username, class: "flex-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300" %> <%= text_input(f, :username,
<.error field={:username} input_name="user[username]" errors={@changeset.errors} class="pt-2 pl-4 pr-4 ml-2 text-center" /> class:
"flex-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300"
) %>
<.error
field={:username}
input_name="user[username]"
errors={@changeset.errors}
class="pt-2 pl-4 pr-4 ml-2 text-center"
/>
</div> </div>
</div> </div>
@ -38,7 +52,11 @@ defmodule LiveBeatsWeb.SettingsLive do
Email (from GitHub) Email (from GitHub)
</label> </label>
<div class="mt-1 flex rounded-md shadow-sm"> <div class="mt-1 flex rounded-md shadow-sm">
<%= text_input f, :email, disabled: true, class: "flex-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300 bg-gray-50" %> <%= text_input(f, :email,
disabled: true,
class:
"flex-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300 bg-gray-50"
) %>
</div> </div>
</div> </div>
@ -47,8 +65,16 @@ defmodule LiveBeatsWeb.SettingsLive do
Profile Tagline Profile Tagline
</label> </label>
<div class="mt-1"> <div class="mt-1">
<%= text_input f, :profile_tagline, class: "flex-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300" %> <%= text_input(f, :profile_tagline,
<.error field={:profile_tagline} input_name="user[profile_tagline]" errors={@changeset.errors} class="pt-2 pl-4 pr-4 ml-2 text-center" /> class:
"flex-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300"
) %>
<.error
field={:profile_tagline}
input_name="user[profile_tagline]"
errors={@changeset.errors}
class="pt-2 pl-4 pr-4 ml-2 text-center"
/>
</div> </div>
<p class="text-sm text-gray-500">Write a short tagline for your beats page.</p> <p class="text-sm text-gray-500">Write a short tagline for your beats page.</p>
</div> </div>
@ -58,7 +84,10 @@ defmodule LiveBeatsWeb.SettingsLive do
<div class="pt-5"> <div class="pt-5">
<div class="flex justify-end"> <div class="flex justify-end">
<button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> <button
type="submit"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Save Save
</button> </button>
</div> </div>

View file

@ -5,7 +5,11 @@ defmodule LiveBeatsWeb.SignInLive do
~H""" ~H"""
<div class="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8"> <div class="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md"> <div class="sm:mx-auto sm:w-full sm:max-w-md">
<img class="mx-auto h-12 w-auto" src="https://tailwindui.com/img/logos/workflow-mark-indigo-600.svg" alt="Workflow"> <img
class="mx-auto h-12 w-auto"
src="https://tailwindui.com/img/logos/workflow-mark-indigo-600.svg"
alt="Workflow"
/>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900"> <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to your account Sign in to your account
</h2> </h2>
@ -20,7 +24,10 @@ defmodule LiveBeatsWeb.SignInLive do
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"> <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<div class="space-y-6"> <div class="space-y-6">
<a href={LiveBeats.Github.authorize_url()} class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> <a
href={LiveBeats.Github.authorize_url()}
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Sign in with GitHub Sign in with GitHub
</a> </a>
</div> </div>

View file

@ -1,28 +1,48 @@
<div id="mobile-sidebar-container" class="fixed inset-0 flex z-40 lg:hidden" aria-modal="true" style="display: none;" role="region"> <div
<div id="mobile-sidebar-container"
class="fixed inset-0 bg-gray-600 bg-opacity-75" class="fixed inset-0 flex z-40 lg:hidden"
phx-click={hide_mobile_sidebar()}> aria-modal="true"
</div> style="display: none;"
role="region"
>
<div class="fixed inset-0 bg-gray-600 bg-opacity-75" phx-click={hide_mobile_sidebar()}></div>
<div id="mobile-sidebar" class="relative flex-1 flex flex-col max-w-xs w-full pt-5 pb-4 bg-white hidden min-h-screen"> <div
id="mobile-sidebar"
class="relative flex-1 flex flex-col max-w-xs w-full pt-5 pb-4 bg-white hidden min-h-screen"
>
<div class="absolute top-0 right-0 -mr-12 pt-2"> <div class="absolute top-0 right-0 -mr-12 pt-2">
<button type="button" <button
type="button"
id="hide-mobile-sidebar" id="hide-mobile-sidebar"
aria-expanded="true" aria-expanded="true"
aria-controls="mobile-sidebar" aria-controls="mobile-sidebar"
class="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" class="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
phx-click={hide_mobile_sidebar()}> phx-click={hide_mobile_sidebar()}
>
<span class="sr-only">Close sidebar</span> <span class="sr-only">Close sidebar</span>
<svg class="h-6 w-6 text-white" xmlns="http://www.w3.org/2000/svg" <svg
fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> class="h-6 w-6 text-white"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
>
</path>
</svg> </svg>
</button> </button>
</div> </div>
<div class="flex-shrink-0 flex items-center px-4"> <div class="flex-shrink-0 flex items-center px-4">
<.link navigate={home_path(@current_user)}> <.link navigate={home_path(@current_user)}>
<.icon name={:status_online} class="w-8 h-8 text-purple-600 -mt-2 inline-block" outlined/> <.icon name={:status_online} class="w-8 h-8 text-purple-600 -mt-2 inline-block" outlined />
<span class="h-8 w-auto text-2xl ml-1 font-bold"> <span class="h-8 w-auto text-2xl ml-1 font-bold">
LiveBeats LiveBeats
</span> </span>
@ -30,14 +50,14 @@
</div> </div>
<div class="mt-5 flex-1 h-0 overflow-y-auto"> <div class="mt-5 flex-1 h-0 overflow-y-auto">
<%= if @current_user do %> <%= if @current_user do %>
<.sidebar_account_dropdown id="mobile-account-dropdown" current_user={@current_user}/> <.sidebar_account_dropdown id="mobile-account-dropdown" current_user={@current_user} />
<% end %> <% end %>
<nav class="px-2"> <nav class="px-2">
<%= if @current_user do %> <%= if @current_user do %>
<.sidebar_nav_links current_user={@current_user} active_tab={@active_tab}/> <.sidebar_nav_links current_user={@current_user} active_tab={@active_tab} />
<% end %> <% end %>
<.sidebar_active_users id="desktop-active-users" users={@active_users}/> <.sidebar_active_users id="desktop-active-users" users={@active_users} />
</nav> </nav>
</div> </div>
</div> </div>
@ -46,14 +66,12 @@
<!-- Dummy element to force sidebar to shrink to fit close icon --> <!-- Dummy element to force sidebar to shrink to fit close icon -->
</div> </div>
</div> </div>
<!-- Static sidebar for desktop --> <!-- Static sidebar for desktop -->
<div class="hidden lg:flex lg:flex-shrink-0" role="region"> <div class="hidden lg:flex lg:flex-shrink-0" role="region">
<div class="flex flex-col w-64 border-r border-gray-200 pt-5 pb-4 bg-gray-100"> <div class="flex flex-col w-64 border-r border-gray-200 pt-5 pb-4 bg-gray-100">
<div class="flex items-center flex-shrink-0 px-6"> <div class="flex items-center flex-shrink-0 px-6">
<.link navigate={home_path(@current_user)}> <.link navigate={home_path(@current_user)}>
<.icon name={:status_online} class="w-8 h-8 text-purple-600 -mt-2 inline-block" outlined/> <.icon name={:status_online} class="w-8 h-8 text-purple-600 -mt-2 inline-block" outlined />
<span class="h-8 w-auto text-2xl ml-1 font-bold"> <span class="h-8 w-auto text-2xl ml-1 font-bold">
LiveBeats LiveBeats
</span> </span>
@ -62,32 +80,48 @@
<!-- Sidebar component, swap this element with another sidebar if you like --> <!-- Sidebar component, swap this element with another sidebar if you like -->
<div class="h-0 flex-1 flex flex-col overflow-y-auto"> <div class="h-0 flex-1 flex flex-col overflow-y-auto">
<%= if @current_user do %> <%= if @current_user do %>
<.sidebar_account_dropdown id="account-dropdown" current_user={@current_user}/> <.sidebar_account_dropdown id="account-dropdown" current_user={@current_user} />
<% end %> <% end %>
<!-- Sidebar Search --> <!-- Sidebar Search -->
<div class="px-3 mt-5"> <div class="px-3 mt-5">
<label for="search" class="sr-only">Search</label> <label for="search" class="sr-only">Search</label>
<div class="mt-1 relative rounded-md shadow-sm"> <div class="mt-1 relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none" aria-hidden="true"> <div
<svg class="mr-3 h-4 w-4 text-gray-400" x-description="Heroicon name: solid/search" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> aria-hidden="true"
<path fill-rule="evenodd" >
<svg
class="mr-3 h-4 w-4 text-gray-400"
x-description="Heroicon name: solid/search"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</div> </div>
<input type="text" name="search" id="search" <input
type="text"
name="search"
id="search"
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-9 sm:text-sm border-gray-300 rounded-md" class="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-9 sm:text-sm border-gray-300 rounded-md"
placeholder="Search"> placeholder="Search"
/>
</div> </div>
</div> </div>
<!-- Navigation --> <!-- Navigation -->
<nav class="px-3 mt-6"> <nav class="px-3 mt-6">
<%= if @current_user do %> <%= if @current_user do %>
<.sidebar_nav_links current_user={@current_user} active_tab={@active_tab}/> <.sidebar_nav_links current_user={@current_user} active_tab={@active_tab} />
<% end %> <% end %>
<!-- Secondary navigation --> <!-- Secondary navigation -->
<.sidebar_active_users id="mobile-active-users" users={@active_users}/> <.sidebar_active_users id="mobile-active-users" users={@active_users} />
</nav> </nav>
</div> </div>
</div> </div>
@ -95,17 +129,34 @@
<!-- Main column --> <!-- Main column -->
<div class="flex flex-col w-0 flex-1 overflow-hidden"> <div class="flex flex-col w-0 flex-1 overflow-hidden">
<!-- Search header --> <!-- Search header -->
<div class="relative z-10 flex-shrink-0 flex h-16 bg-white border-b border-gray-200 lg:hidden" role="navigation"> <div
<button type="button" class="relative z-10 flex-shrink-0 flex h-16 bg-white border-b border-gray-200 lg:hidden"
role="navigation"
>
<button
type="button"
id="show-mobile-sidebar" id="show-mobile-sidebar"
aria-expanded="false" aria-expanded="false"
aria-controls="mobile-sidebar" aria-controls="mobile-sidebar"
class="px-4 border-r border-gray-200 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-purple-500 lg:hidden" class="px-4 border-r border-gray-200 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-purple-500 lg:hidden"
phx-click={show_mobile_sidebar()}> phx-click={show_mobile_sidebar()}
>
<span class="sr-only">Open sidebar</span> <span class="sr-only">Open sidebar</span>
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" <svg
fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> class="h-6 w-6"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16"></path> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h8m-8 6h16"
>
</path>
</svg> </svg>
</button> </button>
<div class="flex-1 flex justify-between px-4 sm:px-6 lg:px-8"> <div class="flex-1 flex justify-between px-4 sm:px-6 lg:px-8">
@ -114,16 +165,28 @@
<label for="search-field" class="sr-only">Search</label> <label for="search-field" class="sr-only">Search</label>
<div class="relative w-full text-gray-400 focus-within:text-gray-600"> <div class="relative w-full text-gray-400 focus-within:text-gray-600">
<div class="absolute inset-y-0 left-0 flex items-center pointer-events-none"> <div class="absolute inset-y-0 left-0 flex items-center pointer-events-none">
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" <svg
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> class="h-5 w-5"
<path fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</div> </div>
<input id="search-field" name="search-field" <input
id="search-field"
name="search-field"
class="block w-full h-full pl-8 pr-3 py-2 border-transparent text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-0 focus:border-transparent focus:placeholder-gray-400 sm:text-sm" class="block w-full h-full pl-8 pr-3 py-2 border-transparent text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-0 focus:border-transparent focus:placeholder-gray-400 sm:text-sm"
placeholder="Search" type="search"> placeholder="Search"
type="search"
/>
</div> </div>
</form> </form>
</div> </div>
@ -132,15 +195,16 @@
<!-- Profile dropdown TODO --> <!-- Profile dropdown TODO -->
<div class="ml-3 relative"> <div class="ml-3 relative">
<div> <div>
<button type="button" <button
type="button"
class="max-w-xs bg-white flex items-center text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" class="max-w-xs bg-white flex items-center text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
id="user-menu-button" @click="open = true" id="user-menu-button"
aria-expanded="false" aria-haspopup="true" @click="open = true"
aria-expanded="false"
aria-haspopup="true"
> >
<span class="sr-only">Open user menu</span> <span class="sr-only">Open user menu</span>
<img class="h-8 w-8 rounded-full" <img class="h-8 w-8 rounded-full" src={@current_user.avatar_url} alt="" />
src={@current_user.avatar_url}
alt="">
</button> </button>
</div> </div>
</div> </div>
@ -149,8 +213,8 @@
</div> </div>
</div> </div>
<.flash flash={@flash} kind={:info}/> <.flash flash={@flash} kind={:info} />
<.flash flash={@flash} kind={:error}/> <.flash flash={@flash} kind={:error} />
<.connection_status> <.connection_status>
Re-establishing connection... Re-establishing connection...
</.connection_status> </.connection_status>
@ -165,9 +229,18 @@
<%= @inner_content %> <%= @inner_content %>
</main> </main>
<div class="relative"> <div class="relative">
<div id="ping-container" class="fixed bottom-0 right-0 bg-gray-900 text-gray-200 px-2 rounded-tl-md text-sm w-[114px] min-w-max" phx-update="ignore"> <div
id="ping-container"
class="fixed bottom-0 right-0 bg-gray-900 text-gray-200 px-2 rounded-tl-md text-sm w-[114px] min-w-max"
phx-update="ignore"
>
<span id="ping" phx-hook="Ping"></span> <span id="ping" phx-hook="Ping"></span>
<%= if @region do %><img class="inline w-5 h-5 absolute right-0" src={"https://fly.io/ui/images/#{@region}.svg"} /><% end %> <%= if @region do %>
<img
class="inline w-5 h-5 absolute right-0"
src={"https://fly.io/ui/images/#{@region}.svg"}
/>
<% end %>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,13 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<%= csrf_meta_tag() %> <%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "LiveBeats", suffix: " · Phoenix Framework" %> <%= live_title_tag(assigns[:page_title] || "LiveBeats", suffix: " · Phoenix Framework") %>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/> <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")} />
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script> <script
defer
phx-track-static
type="text/javascript"
src={Routes.static_path(@conn, "/assets/app.js")}
>
</script>
</head> </head>
<body> <body>
<%= @inner_content %> <%= @inner_content %>

View file

@ -1,15 +1,16 @@
<!-- Pinned projects --> <!-- Pinned projects -->
<div class="px-4 mt-6 sm:px-6 lg:px-8"> <div class="px-4 mt-6 sm:px-6 lg:px-8">
<h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide">Who's Here</h2> <h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide">Who's Here</h2>
<ul role="list" class="grid grid-cols-1 gap-4 sm:gap-6 sm:grid-cols-2 xl:grid-cols-4 mt-3" x-max="1"> <ul
role="list"
class="grid grid-cols-1 gap-4 sm:gap-6 sm:grid-cols-2 xl:grid-cols-4 mt-3"
x-max="1"
>
<li class="relative col-span-1 flex shadow-sm rounded-md"> <li class="relative col-span-1 flex shadow-sm rounded-md">
<div <div class="flex-shrink-0 flex items-center justify-center w-16 bg-pink-600 text-white text-sm font-medium rounded-l-md">
class="flex-shrink-0 flex items-center justify-center w-16 bg-pink-600 text-white text-sm font-medium rounded-l-md">
CM CM
</div> </div>
<div <div class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate">
class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate">
<div class="flex-1 px-4 py-2 text-sm truncate"> <div class="flex-1 px-4 py-2 text-sm truncate">
<a href="#" class="text-gray-900 font-medium hover:text-gray-600"> <a href="#" class="text-gray-900 font-medium hover:text-gray-600">
Chris Chris
@ -20,12 +21,10 @@
</li> </li>
<li class="relative col-span-1 flex shadow-sm rounded-md"> <li class="relative col-span-1 flex shadow-sm rounded-md">
<div <div class="flex-shrink-0 flex items-center justify-center w-16 bg-purple-600 text-white text-sm font-medium rounded-l-md">
class="flex-shrink-0 flex items-center justify-center w-16 bg-purple-600 text-white text-sm font-medium rounded-l-md">
KM KM
</div> </div>
<div <div class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate">
class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate">
<div class="flex-1 px-4 py-2 text-sm truncate"> <div class="flex-1 px-4 py-2 text-sm truncate">
<a href="#" class="text-gray-900 font-medium hover:text-gray-600"> <a href="#" class="text-gray-900 font-medium hover:text-gray-600">
Kurt Kurt
@ -36,12 +35,10 @@
</li> </li>
<li class="relative col-span-1 flex shadow-sm rounded-md"> <li class="relative col-span-1 flex shadow-sm rounded-md">
<div <div class="flex-shrink-0 flex items-center justify-center w-16 bg-green-600 text-white text-sm font-medium rounded-l-md">
class="flex-shrink-0 flex items-center justify-center w-16 bg-green-600 text-white text-sm font-medium rounded-l-md">
JV JV
</div> </div>
<div <div class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate">
class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate">
<div class="flex-1 px-4 py-2 text-sm truncate"> <div class="flex-1 px-4 py-2 text-sm truncate">
<a href="#" class="text-gray-900 font-medium hover:text-gray-600"> <a href="#" class="text-gray-900 font-medium hover:text-gray-600">
José José
@ -52,327 +49,472 @@
</li> </li>
</ul> </ul>
</div> </div>
<!-- Projects list (only on smallest breakpoint) --> <!-- Projects list (only on smallest breakpoint) -->
<div class="mt-10 sm:hidden"> <div class="mt-10 sm:hidden">
<div class="px-4 sm:px-6"> <div class="px-4 sm:px-6">
<h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide">Projects</h2> <h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide">Projects</h2>
</div> </div>
<ul role="list" class="mt-3 border-t border-gray-200 divide-y divide-gray-100"> <ul role="list" class="mt-3 border-t border-gray-200 divide-y divide-gray-100">
<li> <li>
<a href="#" class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"> <a
href="#"
class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"
>
<span class="flex items-center truncate space-x-3"> <span class="flex items-center truncate space-x-3">
<span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-pink-600" aria-hidden="true"></span> <span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-pink-600" aria-hidden="true">
</span>
<span class="font-medium truncate text-sm leading-6"> <span class="font-medium truncate text-sm leading-6">
GraphQL API GraphQL API
<!-- space --> <!-- space -->
<span class="truncate font-normal text-gray-500">in Engineering</span> <span class="truncate font-normal text-gray-500">in Engineering</span>
</span> </span>
</span> </span>
<svg class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500" <svg
x-description="Heroicon name: solid/chevron-right" xmlns="http://www.w3.org/2000/svg" class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500"
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> x-description="Heroicon name: solid/chevron-right"
<path fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</a> </a>
</li> </li>
<li> <li>
<a href="#" class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"> <a
href="#"
class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"
>
<span class="flex items-center truncate space-x-3"> <span class="flex items-center truncate space-x-3">
<span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-purple-600" aria-hidden="true"></span> <span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-purple-600" aria-hidden="true">
</span>
<span class="font-medium truncate text-sm leading-6"> <span class="font-medium truncate text-sm leading-6">
New Benefits Plan New Benefits Plan
<!-- space --> <!-- space -->
<span class="truncate font-normal text-gray-500">in Human Resources</span> <span class="truncate font-normal text-gray-500">in Human Resources</span>
</span> </span>
</span> </span>
<svg class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500" <svg
x-description="Heroicon name: solid/chevron-right" xmlns="http://www.w3.org/2000/svg" class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500"
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> x-description="Heroicon name: solid/chevron-right"
<path fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</a> </a>
</li> </li>
<li> <li>
<a href="#" class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"> <a
href="#"
class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"
>
<span class="flex items-center truncate space-x-3"> <span class="flex items-center truncate space-x-3">
<span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-yellow-500" aria-hidden="true"></span> <span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-yellow-500" aria-hidden="true">
</span>
<span class="font-medium truncate text-sm leading-6"> <span class="font-medium truncate text-sm leading-6">
Onboarding Emails Onboarding Emails
<!-- space --> <!-- space -->
<span class="truncate font-normal text-gray-500">in Customer Success</span> <span class="truncate font-normal text-gray-500">in Customer Success</span>
</span> </span>
</span> </span>
<svg class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500" <svg
x-description="Heroicon name: solid/chevron-right" xmlns="http://www.w3.org/2000/svg" class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500"
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> x-description="Heroicon name: solid/chevron-right"
<path fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</a> </a>
</li> </li>
<li> <li>
<a href="#" class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"> <a
href="#"
class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"
>
<span class="flex items-center truncate space-x-3"> <span class="flex items-center truncate space-x-3">
<span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-green-500" aria-hidden="true"></span> <span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-green-500" aria-hidden="true">
</span>
<span class="font-medium truncate text-sm leading-6"> <span class="font-medium truncate text-sm leading-6">
iOS App iOS App
<!-- space --> <!-- space -->
<span class="truncate font-normal text-gray-500">in Engineering</span> <span class="truncate font-normal text-gray-500">in Engineering</span>
</span> </span>
</span> </span>
<svg class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500" <svg
x-description="Heroicon name: solid/chevron-right" xmlns="http://www.w3.org/2000/svg" class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500"
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> x-description="Heroicon name: solid/chevron-right"
<path fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</a> </a>
</li> </li>
<li> <li>
<a href="#" class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"> <a
href="#"
class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"
>
<span class="flex items-center truncate space-x-3"> <span class="flex items-center truncate space-x-3">
<span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-pink-600" aria-hidden="true"></span> <span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-pink-600" aria-hidden="true">
</span>
<span class="font-medium truncate text-sm leading-6"> <span class="font-medium truncate text-sm leading-6">
Marketing Site Redesign Marketing Site Redesign
<!-- space --> <!-- space -->
<span class="truncate font-normal text-gray-500">in Engineering</span> <span class="truncate font-normal text-gray-500">in Engineering</span>
</span> </span>
</span> </span>
<svg class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500" <svg
x-description="Heroicon name: solid/chevron-right" xmlns="http://www.w3.org/2000/svg" class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500"
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> x-description="Heroicon name: solid/chevron-right"
<path fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</a> </a>
</li> </li>
<li> <li>
<a href="#" class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"> <a
href="#"
class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"
>
<span class="flex items-center truncate space-x-3"> <span class="flex items-center truncate space-x-3">
<span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-purple-600" aria-hidden="true"></span> <span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-purple-600" aria-hidden="true">
</span>
<span class="font-medium truncate text-sm leading-6"> <span class="font-medium truncate text-sm leading-6">
Hire CFO Hire CFO
<!-- space --> <!-- space -->
<span class="truncate font-normal text-gray-500">in Human Resources</span> <span class="truncate font-normal text-gray-500">in Human Resources</span>
</span> </span>
</span> </span>
<svg class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500" <svg
x-description="Heroicon name: solid/chevron-right" xmlns="http://www.w3.org/2000/svg" class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500"
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> x-description="Heroicon name: solid/chevron-right"
<path fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</a> </a>
</li> </li>
<li> <li>
<a href="#" class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"> <a
href="#"
class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"
>
<span class="flex items-center truncate space-x-3"> <span class="flex items-center truncate space-x-3">
<span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-yellow-500" aria-hidden="true"></span> <span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-yellow-500" aria-hidden="true">
</span>
<span class="font-medium truncate text-sm leading-6"> <span class="font-medium truncate text-sm leading-6">
Android App Android App
<!-- space --> <!-- space -->
<span class="truncate font-normal text-gray-500">in Engineering</span> <span class="truncate font-normal text-gray-500">in Engineering</span>
</span> </span>
</span> </span>
<svg class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500" <svg
x-description="Heroicon name: solid/chevron-right" xmlns="http://www.w3.org/2000/svg" class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500"
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> x-description="Heroicon name: solid/chevron-right"
<path fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</a> </a>
</li> </li>
<li> <li>
<a href="#" class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"> <a
href="#"
class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"
>
<span class="flex items-center truncate space-x-3"> <span class="flex items-center truncate space-x-3">
<span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-green-500" aria-hidden="true"></span> <span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-green-500" aria-hidden="true">
</span>
<span class="font-medium truncate text-sm leading-6"> <span class="font-medium truncate text-sm leading-6">
New Customer Portal New Customer Portal
<!-- space --> <!-- space -->
<span class="truncate font-normal text-gray-500">in Engineering</span> <span class="truncate font-normal text-gray-500">in Engineering</span>
</span> </span>
</span> </span>
<svg class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500" <svg
x-description="Heroicon name: solid/chevron-right" xmlns="http://www.w3.org/2000/svg" class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500"
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> x-description="Heroicon name: solid/chevron-right"
<path fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</a> </a>
</li> </li>
<li> <li>
<a href="#" class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"> <a
href="#"
class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"
>
<span class="flex items-center truncate space-x-3"> <span class="flex items-center truncate space-x-3">
<span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-pink-600" aria-hidden="true"></span> <span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-pink-600" aria-hidden="true">
</span>
<span class="font-medium truncate text-sm leading-6"> <span class="font-medium truncate text-sm leading-6">
Co-op Program Co-op Program
<!-- space --> <!-- space -->
<span class="truncate font-normal text-gray-500">in Human Resources</span> <span class="truncate font-normal text-gray-500">in Human Resources</span>
</span> </span>
</span> </span>
<svg class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500" <svg
x-description="Heroicon name: solid/chevron-right" xmlns="http://www.w3.org/2000/svg" class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500"
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> x-description="Heroicon name: solid/chevron-right"
<path fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</a> </a>
</li> </li>
<li> <li>
<a href="#" class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"> <a
href="#"
class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"
>
<span class="flex items-center truncate space-x-3"> <span class="flex items-center truncate space-x-3">
<span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-purple-600" aria-hidden="true"></span> <span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-purple-600" aria-hidden="true">
</span>
<span class="font-medium truncate text-sm leading-6"> <span class="font-medium truncate text-sm leading-6">
Implement NPS Implement NPS
<!-- space --> <!-- space -->
<span class="truncate font-normal text-gray-500">in Customer Success</span> <span class="truncate font-normal text-gray-500">in Customer Success</span>
</span> </span>
</span> </span>
<svg class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500" <svg
x-description="Heroicon name: solid/chevron-right" xmlns="http://www.w3.org/2000/svg" class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500"
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> x-description="Heroicon name: solid/chevron-right"
<path fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</a> </a>
</li> </li>
<li> <li>
<a href="#" class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"> <a
href="#"
class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"
>
<span class="flex items-center truncate space-x-3"> <span class="flex items-center truncate space-x-3">
<span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-yellow-500" aria-hidden="true"></span> <span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-yellow-500" aria-hidden="true">
</span>
<span class="font-medium truncate text-sm leading-6"> <span class="font-medium truncate text-sm leading-6">
Employee Recognition Program Employee Recognition Program
<!-- space --> <!-- space -->
<span class="truncate font-normal text-gray-500">in Human Resources</span> <span class="truncate font-normal text-gray-500">in Human Resources</span>
</span> </span>
</span> </span>
<svg class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500" <svg
x-description="Heroicon name: solid/chevron-right" xmlns="http://www.w3.org/2000/svg" class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500"
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> x-description="Heroicon name: solid/chevron-right"
<path fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</a> </a>
</li> </li>
<li> <li>
<a href="#" class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"> <a
href="#"
class="group flex items-center justify-between px-4 py-4 hover:bg-gray-50 sm:px-6"
>
<span class="flex items-center truncate space-x-3"> <span class="flex items-center truncate space-x-3">
<span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-green-500" aria-hidden="true"></span> <span class="w-2.5 h-2.5 flex-shrink-0 rounded-full bg-green-500" aria-hidden="true">
</span>
<span class="font-medium truncate text-sm leading-6"> <span class="font-medium truncate text-sm leading-6">
Open Source Web Client Open Source Web Client
<!-- space --> <!-- space -->
<span class="truncate font-normal text-gray-500">in Engineering</span> <span class="truncate font-normal text-gray-500">in Engineering</span>
</span> </span>
</span> </span>
<svg class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500" <svg
x-description="Heroicon name: solid/chevron-right" xmlns="http://www.w3.org/2000/svg" class="ml-4 h-5 w-5 text-gray-400 group-hover:text-gray-500"
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> x-description="Heroicon name: solid/chevron-right"
<path fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"
>
</path>
</svg> </svg>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<!-- Songs table (small breakpoint and up) --> <!-- Songs table (small breakpoint and up) -->
<div class="hidden mt-8 sm:block"> <div class="hidden mt-8 sm:block">
<div class="align-middle inline-block min-w-full border-b border-gray-200"> <div class="align-middle inline-block min-w-full border-b border-gray-200">
<table class="min-w-full"> <table class="min-w-full">
<thead> <thead>
<tr class="border-t border-gray-200"> <tr class="border-t border-gray-200">
<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">
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> <span class="lg:pl-2">Nextup</span>
</th> </th>
<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">
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 likes
</th> </th>
<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">
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 user
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="bg-white divide-y divide-gray-100"> <tbody class="bg-white divide-y divide-gray-100">
<%= for _ <- 1..20 do %> <%= for _ <- 1..20 do %>
<tr> <tr>
<td class="px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900"> <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 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> <div class="flex-shrink-0 w-2.5 h-2.5 rounded-full bg-pink-600" aria-hidden="true">
<a href="#" class="truncate hover:text-gray-600"> </div>
<span> <a href="#" class="truncate hover:text-gray-600">
GraphQL API <span>
GraphQL API
<!-- space --> <!-- space -->
<span class="text-gray-500 font-normal">in Engineering</span> <span class="text-gray-500 font-normal">in Engineering</span>
</span> </span>
</a> </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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80"
alt="Tom Cook">
</div> </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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80"
alt="Dries Vincent"
/>
<span class="flex-shrink-0 text-xs leading-5 font-medium">+8</span> <img
</div> class="max-w-none h-6 w-6 rounded-full ring-2 ring-white"
</td> src="https://images.unsplash.com/photo-1517841905240-472988babdf9?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80"
<td class="hidden md:table-cell px-6 py-3 whitespace-nowrap text-sm text-gray-500 text-right"> alt="Lindsay Walton"
March 17, 2020 />
</td>
</tr> <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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;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 %> <% end %>
<tr> <tr>
<td class="px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900"> <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 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> <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"> <a href="#" class="truncate hover:text-gray-600">
<span> <span>
New Benefits Plan New Benefits Plan
@ -385,23 +527,29 @@
<td class="px-6 py-3 text-sm text-gray-500 font-medium"> <td class="px-6 py-3 text-sm text-gray-500 font-medium">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<div class="flex flex-shrink-0 -space-x-1"> <div class="flex flex-shrink-0 -space-x-1">
<img
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white" 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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80" src="https://images.unsplash.com/photo-1519345182560-3f2917c472ef?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80"
alt="Leonard Krasner"> alt="Leonard Krasner"
/>
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white" <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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80" src="https://images.unsplash.com/photo-1463453091185-61582044d556?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80"
alt="Floyd Miles"> alt="Floyd Miles"
/>
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white" <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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80" src="https://images.unsplash.com/photo-1502685104226-ee32379fefbe?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80"
alt="Emily Selman"> alt="Emily Selman"
/>
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white" <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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80" src="https://images.unsplash.com/photo-1500917293891-ef795e70e1f6?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80"
alt="Kristin Watson"> alt="Kristin Watson"
/>
</div> </div>
<span class="flex-shrink-0 text-xs leading-5 font-medium">+4</span> <span class="flex-shrink-0 text-xs leading-5 font-medium">+4</span>
@ -415,7 +563,8 @@
<tr> <tr>
<td class="px-6 py-3 max-w-0 w-full whitespace-nowrap text-sm font-medium text-gray-900"> <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 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> <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"> <a href="#" class="truncate hover:text-gray-600">
<span> <span>
Onboarding Emails Onboarding Emails
@ -428,23 +577,29 @@
<td class="px-6 py-3 text-sm text-gray-500 font-medium"> <td class="px-6 py-3 text-sm text-gray-500 font-medium">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<div class="flex flex-shrink-0 -space-x-1"> <div class="flex flex-shrink-0 -space-x-1">
<img
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white" 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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80" src="https://images.unsplash.com/photo-1502685104226-ee32379fefbe?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80"
alt="Emily Selman"> alt="Emily Selman"
/>
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white" <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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80" src="https://images.unsplash.com/photo-1500917293891-ef795e70e1f6?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80"
alt="Kristin Watson"> alt="Kristin Watson"
/>
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white" <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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80" src="https://images.unsplash.com/photo-1505840717430-882ce147ef2d?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80"
alt="Emma Dorsey"> alt="Emma Dorsey"
/>
<img class="max-w-none h-6 w-6 rounded-full ring-2 ring-white" <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&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80" src="https://images.unsplash.com/photo-1509783236416-c9ad59bae472?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80"
alt="Alicia Bell"> alt="Alicia Bell"
/>
</div> </div>
<span class="flex-shrink-0 text-xs leading-5 font-medium">+10</span> <span class="flex-shrink-0 text-xs leading-5 font-medium">+10</span>
@ -457,4 +612,4 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>

View file

@ -24,7 +24,9 @@ defmodule LiveBeatsWeb.ErrorHelpers do
<%= for error <- @error_values do %> <%= for error <- @error_values do %>
<span <span
phx-feedback-for={@input_name} phx-feedback-for={@input_name}
class={"invalid-feedback inline-block pl-2 pr-2 text-sm text-white bg-red-600 rounded-md #{@class}"} class={
"invalid-feedback inline-block pl-2 pr-2 text-sm text-white bg-red-600 rounded-md #{@class}"
}
> >
<%= translate_error(error) %> <%= translate_error(error) %>
</span> </span>

View file

@ -16,7 +16,8 @@ defmodule LiveBeatsWeb.LayoutView do
</h3> </h3>
<div class="mt-1 space-y-1" role="group" aria-labelledby={@id}> <div class="mt-1 space-y-1" role="group" aria-labelledby={@id}>
<%= for user <- @users do %> <%= for user <- @users do %>
<.link navigate={profile_path(user)} <.link
navigate={profile_path(user)}
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" 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="w-2.5 h-2.5 mr-4 bg-indigo-500 rounded-full" aria-hidden="true"></span>
@ -36,30 +37,51 @@ defmodule LiveBeatsWeb.LayoutView do
<%= if @current_user do %> <%= if @current_user do %>
<.link <.link
navigate={profile_path(@current_user)} navigate={profile_path(@current_user)}
class={"text-gray-700 hover:text-gray-900 group flex items-center px-2 py-2 text-sm font-medium rounded-md #{if @active_tab == :profile, do: "bg-gray-200", else: "hover:bg-gray-50"}"} class={
"text-gray-700 hover:text-gray-900 group flex items-center px-2 py-2 text-sm font-medium rounded-md #{if @active_tab == :profile, do: "bg-gray-200", else: "hover:bg-gray-50"}"
}
aria-current={if @active_tab == :profile, do: "true", else: "false"} aria-current={if @active_tab == :profile, do: "true", else: "false"}
> >
<.icon name={:music_note} outlined class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-6 w-6"/> <.icon
My Songs name={:music_note}
outlined
class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-6 w-6"
/> My Songs
</.link> </.link>
<.link <.link
navigate={Routes.settings_path(Endpoint, :edit)} navigate={Routes.settings_path(Endpoint, :edit)}
class={"text-gray-700 hover:text-gray-900 group flex items-center px-2 py-2 text-sm font-medium rounded-md #{if @active_tab == :settings, do: "bg-gray-200", else: "hover:bg-gray-50"}"} class={
"text-gray-700 hover:text-gray-900 group flex items-center px-2 py-2 text-sm font-medium rounded-md #{if @active_tab == :settings, do: "bg-gray-200", else: "hover:bg-gray-50"}"
}
aria-current={if @active_tab == :settings, do: "true", else: "false"} aria-current={if @active_tab == :settings, do: "true", else: "false"}
> >
<.icon name={:adjustments} outlined class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-6 w-6"/> <.icon
Settings name={:adjustments}
outlined
class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-6 w-6"
/> Settings
</.link> </.link>
<% else %> <% else %>
<.link navigate={Routes.sign_in_path(Endpoint, :index)} <.link
navigate={Routes.sign_in_path(Endpoint, :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" 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"
> >
<svg class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-6 w-6" <svg
xmlns="http://www.w3.org/2000/svg" fill="none" class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-6 w-6"
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> xmlns="http://www.w3.org/2000/svg"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" fill="none"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path> viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
>
</path>
</svg> </svg>
Sign in Sign in
</.link> </.link>
@ -71,10 +93,9 @@ defmodule LiveBeatsWeb.LayoutView do
def sidebar_account_dropdown(assigns) do def sidebar_account_dropdown(assigns) do
~H""" ~H"""
<.dropdown id={@id} foo="bar"> <.dropdown id={@id} foo="bar">
<:img src={@current_user.avatar_url}/> <:img src={@current_user.avatar_url} />
<:title><%= @current_user.name %></:title> <:title><%= @current_user.name %></:title>
<:subtitle>@<%= @current_user.username %></:subtitle> <:subtitle>@<%= @current_user.username %></:subtitle>
<:link navigate={profile_path(@current_user)}>View Profile</:link> <:link navigate={profile_path(@current_user)}>View Profile</:link>
<:link navigate={Routes.settings_path(Endpoint, :edit)}>Settings</:link> <:link navigate={Routes.settings_path(Endpoint, :edit)}>Settings</:link>
<:link href={Routes.o_auth_callback_path(Endpoint, :sign_out)} method={:delete}>Sign out</:link> <:link href={Routes.o_auth_callback_path(Endpoint, :sign_out)} method={:delete}>Sign out</:link>

View file

@ -22,12 +22,12 @@
"libcluster": {:hex, :libcluster, "3.3.1", "e7a4875cd1290cee7a693d6bd46076863e9e433708b01339783de6eff5b7f0aa", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b575ca63c1cd84e01f3fa0fc45e6eb945c1ee7ae8d441d33def999075e9e5398"}, "libcluster": {:hex, :libcluster, "3.3.1", "e7a4875cd1290cee7a693d6bd46076863e9e433708b01339783de6eff5b7f0aa", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b575ca63c1cd84e01f3fa0fc45e6eb945c1ee7ae8d441d33def999075e9e5398"},
"mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"},
"mint": {:hex, :mint, "1.3.0", "396b3301102f7b775e103da5a20494b25753aed818d6d6f0ad222a3a018c3600", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "a9aac960562e43ca69a77e5176576abfa78b8398cec5543dd4fb4ab0131d5c1e"}, "mint": {:hex, :mint, "1.3.0", "396b3301102f7b775e103da5a20494b25753aed818d6d6f0ad222a3a018c3600", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "a9aac960562e43ca69a77e5176576abfa78b8398cec5543dd4fb4ab0131d5c1e"},
"phoenix": {:git, "https://github.com/phoenixframework/phoenix.git", "c5bb03eee8ad802c125c92202ebec4e15c35c3e8", []}, "phoenix": {:git, "https://github.com/phoenixframework/phoenix.git", "063bb8c37b05a61b6f411ef4b678efe0cd4e3841", []},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.2", "0769470265eb13af01b5001b29cb935f4710d6adaa1ffc18417a570a337a2f0f", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5bc6c6b38a2ca8b5020b442322fcee6afd5e641637a0b1fb059d4bd89bc58e7b"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.2", "0769470265eb13af01b5001b29cb935f4710d6adaa1ffc18417a570a337a2f0f", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5bc6c6b38a2ca8b5020b442322fcee6afd5e641637a0b1fb059d4bd89bc58e7b"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
"phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "8bae1b3944c2cd9c3078839a980eca56cc59d449", []}, "phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "aab28ef96bf79a4218c1402a4aef6c3613380e1a", []},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
"phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"}, "phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"},
"plug": {:hex, :plug, "1.13.4", "addb6e125347226e3b11489e23d22a60f7ab74786befb86c14f94fb5f23ca9a4", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "06114c1f2a334212fe3ae567dbb3b1d29fd492c1a09783d52f3d489c1a6f4cf2"}, "plug": {:hex, :plug, "1.13.4", "addb6e125347226e3b11489e23d22a60f7ab74786befb86c14f94fb5f23ca9a4", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "06114c1f2a334212fe3ae567dbb3b1d29fd492c1a09783d52f3d489c1a6f4cf2"},

View file

@ -4,17 +4,17 @@ defmodule LiveBeats.MP3StatTest do
alias LiveBeats.MP3Stat alias LiveBeats.MP3Stat
test "parse/1 with valid mp3" do test "parse/1 with valid mp3" do
{:ok, %MP3Stat{} = stat} = MP3Stat.parse("test/support/fixtures/silence1s.mp3") {:ok, %MP3Stat{} = stat} = MP3Stat.parse("test/support/fixtures/silence1s.mp3")
assert stat.duration == 1 assert stat.duration == 1
assert stat.title == "Silence" assert stat.title == "Silence"
assert stat.artist == "Anon" assert stat.artist == "Anon"
end end
test "parse/1 with invalid mp3" do test "parse/1 with invalid mp3" do
assert {:error, :bad_file} = MP3Stat.parse("mix.exs") assert {:error, :bad_file} = MP3Stat.parse("mix.exs")
end end
test "parse/1 with missing file" do test "parse/1 with missing file" do
assert {:error, :bad_file} = MP3Stat.parse("lsfjslkfjslkfjs") assert {:error, :bad_file} = MP3Stat.parse("lsfjslkfjslkfjs")
end end
end end

View file

@ -1,6 +1,7 @@
defmodule Phoenix.Presence.ClientTest.Presence do defmodule Phoenix.Presence.ClientTest.Presence do
use Phoenix.Presence, otp_app: :live_beats, use Phoenix.Presence,
pubsub_server: LiveBeats.PubSub otp_app: :live_beats,
pubsub_server: LiveBeats.PubSub
end end
defmodule Phoenix.Presence.ClientTest do defmodule Phoenix.Presence.ClientTest do

View file

@ -9,6 +9,7 @@ defmodule LiveBeats.AccountsFixtures do
def user_fixture(attrs \\ %{}) do def user_fixture(attrs \\ %{}) do
primary_email = attrs[:email] || unique_user_email() primary_email = attrs[:email] || unique_user_email()
info = %{ info = %{
"avatar_url" => "https://avatars3.githubusercontent.com/u/576796?v=4", "avatar_url" => "https://avatars3.githubusercontent.com/u/576796?v=4",
"bio" => nil, "bio" => nil,
@ -43,11 +44,11 @@ defmodule LiveBeats.AccountsFixtures do
"updated_at" => "2020-09-18T19:34:45Z", "updated_at" => "2020-09-18T19:34:45Z",
"url" => "https://api.github.com/users/chrismccord" "url" => "https://api.github.com/users/chrismccord"
} }
emails = [] emails = []
token = "token" token = "token"
{:ok, user} = {:ok, user} = LiveBeats.Accounts.register_github_user(primary_email, info, emails, token)
LiveBeats.Accounts.register_github_user(primary_email, info, emails, token)
user user
end end

View file

@ -1,5 +1,4 @@
defmodule Phoenix.Presence.Client.Mock do defmodule Phoenix.Presence.Client.Mock do
def init(_opts) do def init(_opts) do
{:ok, %{}} {:ok, %{}}
end end
@ -11,5 +10,4 @@ defmodule Phoenix.Presence.Client.Mock do
def handle_leave(_topic, _key, _meta, state) do def handle_leave(_topic, _key, _meta, state) do
{:ok, state} {:ok, state}
end end
end end

View file

@ -2,7 +2,6 @@ defmodule Phoenix.Presence.Client.PresenceMock do
use GenServer use GenServer
alias Phoenix.Presence.Client alias Phoenix.Presence.Client
def start_link(opts \\ []) do def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts[:id], opts) GenServer.start_link(__MODULE__, opts[:id], opts)
end end