Files
music_library/lib/music_library_web/components/stats_components.ex
T
2026-02-10 19:32:28 +00:00

273 lines
8.6 KiB
Elixir

defmodule MusicLibraryWeb.StatsComponents do
use MusicLibraryWeb, :live_component
import MusicLibraryWeb.RecordComponents,
only: [
format_label: 1,
type_label: 1,
artist_links: 1,
record_cover: 1,
release_groups_badge: 1
]
alias MusicLibrary.Records
attr :record, Records.Record, required: false
attr :title, :string, required: true
attr :class, :string
def album_preview(assigns) do
~H"""
<div
:if={@record}
class={[
"flex items-center rounded-md bg-white dark:bg-zinc-800 px-4 pb-5 pt-5 shadow-sm sm:px-6 sm:pt-6 cursor-pointer",
@class
]}
phx-click={JS.navigate(~p"/collection/#{@record}")}
>
<div>
<.record_cover
record={@record}
class="w-20 md:w-24 rounded-md shadow-sm"
width={192}
/>
</div>
<div class="ml-4">
<p class="truncate text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400">
{@title}
</p>
<p class="font-semibold">
<span class="text-sm md:text-base lg:text-2xl block text-zinc-900 dark:text-zinc-300">
{@record.title}
</span>
<.link
:for={artist <- @record.artists}
class="text-sm md:text-base text-zinc-600 dark:text-zinc-300 hover:text-zinc-500 dark:hover:text-zinc-200"
navigate={~p"/artists/#{artist.musicbrainz_id}"}
>
{artist.name}
</.link>
</p>
</div>
</div>
<div
:if={!@record}
class={[
"flex items-center rounded-md bg-white dark:bg-zinc-800 px-4 pb-5 pt-5 shadow-sm sm:px-6 sm:pt-6 cursor-pointer",
@class
]}
>
<p class="truncate text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400">
{gettext("No record found")}
</p>
</div>
"""
end
attr :title, :string, required: true
attr :count, :integer, required: true
attr :path, :string, required: false, default: nil
attr :tooltip, :any, required: false, default: nil
def counter(assigns) do
~H"""
<div class="overflow-hidden rounded-md bg-white dark:bg-zinc-800 shadow-sm flex justify-center items-center">
<div class="p-4 md:p-0">
<dt>
<p class="truncate text-sm font-medium text-center text-zinc-500 dark:text-zinc-400">
{@title}
</p>
</dt>
<dd :if={!@tooltip} class="mt-1">
<.link
:if={@path}
navigate={@path}
class="block text-2xl sm:text-3xl font-semibold text-center text-zinc-900 hover:text-zinc-500 dark:text-zinc-300 dark:hover:text-zinc-200"
>
{@count}
</.link>
<p
:if={!@path}
class="block cursor-default text-2xl sm:text-3xl font-semibold text-center text-zinc-900 dark:text-zinc-300"
>
{@count}
</p>
</dd>
<dd :if={@tooltip} class="mt-1">
<.tooltip>
<:content>
<span id={"#{@title}-counter-tooltip"} phx-hook="FormatNumber">{@tooltip}</span>
</:content>
<.link
:if={@path}
navigate={@path}
class="block text-2xl sm:text-3xl font-semibold text-center text-zinc-900 hover:text-zinc-500 dark:text-zinc-300 dark:hover:text-zinc-200"
>
{@count}
</.link>
<p
:if={!@path}
class="block cursor-default text-2xl sm:text-3xl font-semibold text-center text-zinc-900 dark:text-zinc-300"
>
{@count}
</p>
</.tooltip>
</dd>
</div>
</div>
"""
end
attr :categories_with_counts, :list, required: true
attr :category_format_fn, :any, required: true
attr :category_path_fn, :any, required: true
def counters_by_category(assigns) do
~H"""
<%!-- TODO: replace with OSS version --%>
<dl class={[
"mt-5 grid divide-zinc-200 dark:divide-zinc-900 overflow-hidden rounded-md bg-white dark:bg-zinc-800 shadow-sm divide-x",
stats_class(@categories_with_counts)
]}>
<div :for={{category, count} <- @categories_with_counts} class="px-2 py-5 sm:px-4">
<dt class="text-sm font-medium text-zinc-500 dark:text-zinc-400 text-center max-sm:text-xs break-keep">
{@category_format_fn.(category)}
</dt>
<dd class="mt-1 text-center">
<.link
class="text-xl lg:text-2xl font-semibold hover:text-zinc-500 dark:text-zinc-300 dark:hover:text-zinc-200"
navigate={@category_path_fn.(category)}
>
{count}
</.link>
</dd>
</div>
</dl>
"""
end
attr :record_show_path, :any, required: true
attr :records, :list, required: true
attr :current_date, Date, required: true
def records_on_this_day(assigns) do
~H"""
<ul
class="mt-5 p-4"
role="list"
id="records-on-this-day"
>
<li
id="no-records-on-this-day"
class="hidden py-8 only:flex items-center justify-center text-sm text-zinc-500 dark:text-zinc-400"
>
{gettext("No records released on this day.")}
</li>
<li
:for={record <- @records}
phx-click={JS.navigate(@record_show_path.(record))}
class="flex justify-between gap-x-6 py-2 hover:bg-zinc-50 dark:hover:bg-zinc-700 px-2 md:px-4 cursor-pointer"
id={record.id}
>
<div class="flex min-w-0 gap-x-4 items-center">
<div class="relative w-12 flex-none">
<.record_cover record={record} width={96} />
<.release_groups_badge record={record} />
</div>
<div class="min-w-0 flex-auto">
<h1 class="text-sm leading-6 text-zinc-700">
<.artist_links joinphrase_class="text-xs" artists={record.artists} />
</h1>
<h2 class="mt-1 flex font-semibold text-sm sm:text-base leading-5 text-zinc-700 dark:text-zinc-300 text-wrap">
{record.title}
</h2>
<p class="mt-1 text-xs leading-5 text-zinc-500 dark:text-zinc-400">
<.released_how_long_ago record={record} current_date={@current_date} />
· {format_label(record.format)} · {type_label(record.type)}
<span :if={record.purchased_at}>
·
<span class="sr-only">
{gettext("Purchased on")}
</span>
<.icon
name="hero-banknotes"
class="h-4 w-4"
aria-hidden="true"
data-slot="icon"
/>
{Records.Record.format_as_date(record.purchased_at)}
</span>
</p>
</div>
</div>
</li>
</ul>
"""
end
attr :record, Records.Record, required: true
attr :current_date, Date, required: true
defp released_how_long_ago(assigns) do
assigns =
assign(
assigns,
:years,
Records.Record.released_how_long_ago?(assigns.record, assigns.current_date)
)
~H"""
<span
:if={same_year?(@years)}
class={[
"text-xs leading-5",
"font-semibold bg-linear-to-r bg-clip-text text-transparent from-red-500 via-red-200 to-red-700 animate-shine"
]}
>
{gettext("Today")}
</span>
<span
:if={!same_year?(@years)}
class={[
"text-xs leading-5",
normal_year?(@years) && "text-zinc-500 dark:text-zinc-400",
gold_year?(@years) &&
"font-semibold bg-linear-to-r bg-clip-text text-transparent from-yellow-500 via-yellow-200 to-yellow-700 animate-shine",
silver_year?(@years) &&
"font-semibold bg-linear-to-r bg-clip-text text-transparent from-gray-500 via-gray-200 to-gray-700 animate-shine"
]}
>
{ngettext(
"1 year ago",
"%{count} years ago",
@years
)}
</span>
"""
end
defp same_year?(year), do: year == 0
defp gold_year?(year), do: rem(year, 10) == 0
defp silver_year?(year), do: rem(year, 5) == 0
defp normal_year?(year), do: !gold_year?(year) && !silver_year?(year)
# The Tailwind build step requires all needed classes to be explicitly referenced
# in the source code, and not dynamically generated. This implies that one cannot
# (for example) interpolate a number in a class name.
defp stats_class(collection) do
case Enum.count(collection) do
1 -> "grid-cols-1"
2 -> "grid-cols-2"
3 -> "grid-cols-3"
4 -> "grid-cols-4"
5 -> "grid-cols-5"
6 -> "grid-cols-6"
7 -> "grid-cols-7"
8 -> "grid-cols-8"
9 -> "grid-cols-9"
_other -> ""
end
end
end