Extract shared TopByPeriod LiveComponent

Closes #103
This commit is contained in:
Claudio Ortolina
2026-03-12 07:38:58 +00:00
parent f0151de808
commit 8d9bf78974
5 changed files with 176 additions and 268 deletions
@@ -1,111 +1,27 @@
defmodule MusicLibraryWeb.StatsLive.TopAlbums do
use MusicLibraryWeb, :live_component
use MusicLibraryWeb, :html
alias MusicLibrary.Assets.Transform
alias MusicLibrary.ListeningStats
alias MusicLibraryWeb.StatsLive.TopByPeriod
attr :id, :string, required: true
attr :timezone, :string, required: true
attr :last_updated_uts, :any
def live(assigns) do
~H"""
<.live_component module={__MODULE__} {assigns} id={@id} />
"""
end
@impl true
def render(assigns) do
~H"""
<div>
<h1 class="text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
{gettext("Top Albums")}
</h1>
<.tabs class="mt-4">
<.tabs_list active_tab={name_from_period(@period)} variant="segmented" size="xs">
<:tab
class="flex-1"
name="top_albums_last_7_days"
phx-click={JS.push("set_period", value: %{period: "last_7_days"})}
phx-target={@myself}
>
{gettext("7d")}
</:tab>
<:tab
class="flex-1"
name="top_albums_last_30_days"
phx-click={JS.push("set_period", value: %{period: "last_30_days"})}
phx-target={@myself}
>
{gettext("30d")}
</:tab>
<:tab
class="flex-1"
name="top_albums_last_90_days"
phx-click={JS.push("set_period", value: %{period: "last_90_days"})}
phx-target={@myself}
>
{gettext("90d")}
</:tab>
<:tab
class="flex-1"
name="top_albums_last_365_days"
phx-click={JS.push("set_period", value: %{period: "last_365_days"})}
phx-target={@myself}
>
{gettext("1y")}
</:tab>
<:tab
class="flex-1"
name="top_albums_all_time"
phx-click={JS.push("set_period", value: %{period: "all_time"})}
phx-target={@myself}
>
{gettext("All time")}
</:tab>
</.tabs_list>
<.async_result :let={top_albums} assign={@top_albums}>
<:loading>
<div class="h-182 flex items-center justify-center">
<.loading />
</div>
</:loading>
<.top_albums_by_period albums={top_albums} />
</.async_result>
</.tabs>
</div>
"""
end
defp name_from_period(period), do: "top_albums_#{period}"
@impl true
def mount(socket) do
{:ok,
socket
|> assign(:period, :last_7_days)}
end
@impl true
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_top_albums()}
end
@impl true
def handle_event("set_period", %{"period" => period}, socket) do
{:noreply,
socket
|> assign(:period, String.to_existing_atom(period))
|> assign_top_albums()}
end
attr :albums, :list, required: true
defp top_albums_by_period(assigns) do
~H"""
<div class="mt-4 p-4 bg-white dark:bg-zinc-800 rounded-md shadow-sm">
<div class="space-y-2">
<TopByPeriod.live
id={@id}
timezone={@timezone}
last_updated_uts={@last_updated_uts}
title={gettext("Top Albums")}
key={:top_albums}
fetch_fn={&ListeningStats.get_top_albums_by_period/1}
>
<:item :let={albums}>
<div
:for={album <- @albums}
:for={album <- albums}
phx-click={navigate_to_record(album)}
class={[
"flex items-center space-x-3 p-2",
@@ -151,33 +67,11 @@ defmodule MusicLibraryWeb.StatsLive.TopAlbums do
{album.play_count}
</.badge>
</div>
</div>
</div>
</:item>
</TopByPeriod.live>
"""
end
defp assign_top_albums(socket) do
%{timezone: timezone, period: period} = socket.assigns
current_time = DateTime.utc_now()
assign_async(
socket,
:top_albums,
fn ->
top_albums =
ListeningStats.get_top_albums_by_period(
limit: 10,
current_time: current_time,
timezone: timezone,
period: period
)
{:ok, %{top_albums: top_albums}}
end,
reset: true
)
end
defp navigate_to_record(album) do
cond do
album.collected_record_id ->
@@ -1,112 +1,28 @@
defmodule MusicLibraryWeb.StatsLive.TopArtists do
use MusicLibraryWeb, :live_component
use MusicLibraryWeb, :html
import MusicLibraryWeb.RecordComponents, only: [artist_image: 1]
alias MusicLibrary.ListeningStats
alias MusicLibraryWeb.StatsLive.TopByPeriod
attr :id, :string, required: true
attr :timezone, :string, required: true
attr :last_updated_uts, :any
def live(assigns) do
~H"""
<.live_component module={__MODULE__} {assigns} id={@id} />
"""
end
@impl true
def render(assigns) do
~H"""
<div>
<h1 class="text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
{gettext("Top Artists")}
</h1>
<.tabs class="mt-4">
<.tabs_list active_tab={name_from_period(@period)} variant="segmented" size="xs">
<:tab
class="flex-1"
name="top_artists_last_7_days"
phx-click={JS.push("set_period", value: %{period: "last_7_days"})}
phx-target={@myself}
>
{gettext("7d")}
</:tab>
<:tab
class="flex-1"
name="top_artists_last_30_days"
phx-click={JS.push("set_period", value: %{period: "last_30_days"})}
phx-target={@myself}
>
{gettext("30d")}
</:tab>
<:tab
class="flex-1"
name="top_artists_last_90_days"
phx-click={JS.push("set_period", value: %{period: "last_90_days"})}
phx-target={@myself}
>
{gettext("90d")}
</:tab>
<:tab
class="flex-1"
name="top_artists_last_365_days"
phx-click={JS.push("set_period", value: %{period: "last_365_days"})}
phx-target={@myself}
>
{gettext("1y")}
</:tab>
<:tab
class="flex-1"
name="top_artists_all_time"
phx-click={JS.push("set_period", value: %{period: "all_time"})}
phx-target={@myself}
>
{gettext("All time")}
</:tab>
</.tabs_list>
<.async_result :let={top_artists} assign={@top_artists}>
<:loading>
<div class="h-182 flex items-center justify-center">
<.loading />
</div>
</:loading>
<.top_artists_by_period artists={top_artists} />
</.async_result>
</.tabs>
</div>
"""
end
defp name_from_period(period), do: "top_artists_#{period}"
@impl true
def mount(socket) do
{:ok,
socket
|> assign(:period, :last_7_days)}
end
@impl true
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_top_artists()}
end
@impl true
def handle_event("set_period", %{"period" => period}, socket) do
{:noreply,
socket
|> assign(:period, String.to_existing_atom(period))
|> assign_top_artists()}
end
attr :artists, :list, required: true
def top_artists_by_period(assigns) do
~H"""
<div class="mt-4 p-4 bg-white dark:bg-zinc-800 rounded-md shadow-sm">
<div class="space-y-2">
<TopByPeriod.live
id={@id}
timezone={@timezone}
last_updated_uts={@last_updated_uts}
title={gettext("Top Artists")}
key={:top_artists}
fetch_fn={&ListeningStats.get_top_artists_by_period/1}
>
<:item :let={artists}>
<div
:for={artist <- @artists}
:for={artist <- artists}
phx-click={
artist.musicbrainz_id != "" &&
JS.navigate(~p"/artists/#{artist.musicbrainz_id}")
@@ -139,30 +55,8 @@ defmodule MusicLibraryWeb.StatsLive.TopArtists do
{artist.play_count}
</.badge>
</div>
</div>
</div>
</:item>
</TopByPeriod.live>
"""
end
defp assign_top_artists(socket) do
%{timezone: timezone, period: period} = socket.assigns
current_time = DateTime.utc_now()
assign_async(
socket,
:top_artists,
fn ->
top_artists =
ListeningStats.get_top_artists_by_period(
limit: 10,
current_time: current_time,
timezone: timezone,
period: period
)
{:ok, %{top_artists: top_artists}}
end,
reset: true
)
end
end
@@ -0,0 +1,130 @@
defmodule MusicLibraryWeb.StatsLive.TopByPeriod do
use MusicLibraryWeb, :live_component
attr :id, :string, required: true
attr :title, :string, required: true
attr :key, :atom, required: true
attr :fetch_fn, :any, required: true
attr :timezone, :string, required: true
attr :last_updated_uts, :any
slot :item, required: true
def live(assigns) do
~H"""
<.live_component module={__MODULE__} {assigns} id={@id} />
"""
end
@impl true
def render(assigns) do
~H"""
<div>
<h1 class="text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
{@title}
</h1>
<.tabs class="mt-4">
<.tabs_list active_tab={name_from_period(@key, @period)} variant="segmented" size="xs">
<:tab
class="flex-1"
name={"#{@key}_last_7_days"}
phx-click={JS.push("set_period", value: %{period: "last_7_days"})}
phx-target={@myself}
>
{gettext("7d")}
</:tab>
<:tab
class="flex-1"
name={"#{@key}_last_30_days"}
phx-click={JS.push("set_period", value: %{period: "last_30_days"})}
phx-target={@myself}
>
{gettext("30d")}
</:tab>
<:tab
class="flex-1"
name={"#{@key}_last_90_days"}
phx-click={JS.push("set_period", value: %{period: "last_90_days"})}
phx-target={@myself}
>
{gettext("90d")}
</:tab>
<:tab
class="flex-1"
name={"#{@key}_last_365_days"}
phx-click={JS.push("set_period", value: %{period: "last_365_days"})}
phx-target={@myself}
>
{gettext("1y")}
</:tab>
<:tab
class="flex-1"
name={"#{@key}_all_time"}
phx-click={JS.push("set_period", value: %{period: "all_time"})}
phx-target={@myself}
>
{gettext("All time")}
</:tab>
</.tabs_list>
<.async_result :let={items} assign={assigns[@key]}>
<:loading>
<div class="h-182 flex items-center justify-center">
<.loading />
</div>
</:loading>
<div class="mt-4 p-4 bg-white dark:bg-zinc-800 rounded-md shadow-sm">
<div class="space-y-2">
{render_slot(@item, items)}
</div>
</div>
</.async_result>
</.tabs>
</div>
"""
end
defp name_from_period(key, period), do: "#{key}_#{period}"
@impl true
def mount(socket) do
{:ok, assign(socket, :period, :last_7_days)}
end
@impl true
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_data()}
end
@impl true
def handle_event("set_period", %{"period" => period}, socket) do
{:noreply,
socket
|> assign(:period, String.to_existing_atom(period))
|> assign_data()}
end
defp assign_data(socket) do
%{timezone: timezone, period: period, key: key, fetch_fn: fetch_fn} = socket.assigns
current_time = DateTime.utc_now()
assign_async(
socket,
key,
fn ->
items =
fetch_fn.(
limit: 10,
current_time: current_time,
timezone: timezone,
period: period
)
{:ok, %{key => items}}
end,
reset: true
)
end
end
+5 -10
View File
@@ -851,8 +851,7 @@ msgstr ""
msgid "Top Artists"
msgstr ""
#: lib/music_library_web/live/stats_live/top_albums.ex
#: lib/music_library_web/live/stats_live/top_artists.ex
#: lib/music_library_web/live/stats_live/top_by_period.ex
#, elixir-autogen, elixir-format
msgid "All time"
msgstr ""
@@ -1304,26 +1303,22 @@ msgstr ""
msgid "Read"
msgstr ""
#: lib/music_library_web/live/stats_live/top_albums.ex
#: lib/music_library_web/live/stats_live/top_artists.ex
#: lib/music_library_web/live/stats_live/top_by_period.ex
#, elixir-autogen, elixir-format
msgid "1y"
msgstr ""
#: lib/music_library_web/live/stats_live/top_albums.ex
#: lib/music_library_web/live/stats_live/top_artists.ex
#: lib/music_library_web/live/stats_live/top_by_period.ex
#, elixir-autogen, elixir-format
msgid "30d"
msgstr ""
#: lib/music_library_web/live/stats_live/top_albums.ex
#: lib/music_library_web/live/stats_live/top_artists.ex
#: lib/music_library_web/live/stats_live/top_by_period.ex
#, elixir-autogen, elixir-format
msgid "7d"
msgstr ""
#: lib/music_library_web/live/stats_live/top_albums.ex
#: lib/music_library_web/live/stats_live/top_artists.ex
#: lib/music_library_web/live/stats_live/top_by_period.ex
#, elixir-autogen, elixir-format
msgid "90d"
msgstr ""
+5 -10
View File
@@ -851,8 +851,7 @@ msgstr ""
msgid "Top Artists"
msgstr ""
#: lib/music_library_web/live/stats_live/top_albums.ex
#: lib/music_library_web/live/stats_live/top_artists.ex
#: lib/music_library_web/live/stats_live/top_by_period.ex
#, elixir-autogen, elixir-format
msgid "All time"
msgstr ""
@@ -1304,26 +1303,22 @@ msgstr ""
msgid "Read"
msgstr ""
#: lib/music_library_web/live/stats_live/top_albums.ex
#: lib/music_library_web/live/stats_live/top_artists.ex
#: lib/music_library_web/live/stats_live/top_by_period.ex
#, elixir-autogen, elixir-format
msgid "1y"
msgstr ""
#: lib/music_library_web/live/stats_live/top_albums.ex
#: lib/music_library_web/live/stats_live/top_artists.ex
#: lib/music_library_web/live/stats_live/top_by_period.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "30d"
msgstr ""
#: lib/music_library_web/live/stats_live/top_albums.ex
#: lib/music_library_web/live/stats_live/top_artists.ex
#: lib/music_library_web/live/stats_live/top_by_period.ex
#, elixir-autogen, elixir-format
msgid "7d"
msgstr ""
#: lib/music_library_web/live/stats_live/top_albums.ex
#: lib/music_library_web/live/stats_live/top_artists.ex
#: lib/music_library_web/live/stats_live/top_by_period.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "90d"
msgstr ""