Fix jittering in record detail pages

Use flex instead of columns.
This commit is contained in:
Claudio Ortolina
2025-05-02 21:06:43 +01:00
parent 23e59edeec
commit 38f60d62e0
2 changed files with 491 additions and 487 deletions
@@ -1,4 +1,4 @@
<div class="md:columns-2 mt-4 px-4"> <div class="md:flex mt-4 px-4 md:gap-x-4">
<div class="drop-shadow-sm md:max-w-[38rem]"> <div class="drop-shadow-sm md:max-w-[38rem]">
<img <img
class="w-full rounded-lg drop-shadow-sm" class="w-full rounded-lg drop-shadow-sm"
@@ -7,250 +7,252 @@
/> />
</div> </div>
<div class="mt-4 md:mt-0 flex justify-between"> <div class="grow">
<div> <div class="mt-4 md:mt-0 flex justify-between">
<h1 class="text-base font-medium leading-6 text-zinc-700"> <div>
<.link <h1 class="text-base font-medium leading-6 text-zinc-700">
:for={artist <- @record.artists}
class="text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
navigate={~p"/artists/#{artist.musicbrainz_id}"}
>
{artist.name}
</.link>
</h1>
<h2 class="mt-1 flex font-semibold text-base sm:text-lg leading-5 text-zinc-700 dark:text-zinc-300 text-wrap">
{@record.title}
</h2>
<p class="mt-2 text-sm leading-5 text-zinc-500 dark:text-zinc-400">
{Records.Record.format_release_date(@record.release_date)} · {format_label(@record.format)} · {type_label(
@record.type
)}
</p>
</div>
<div class="relative flex-none">
<button
type="button"
class="text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-300"
aria-expanded="false"
aria-haspopup="true"
phx-click={toggle_actions_menu(@record.id)}
>
<span class="sr-only">{gettext("Open options")}</span>
<.icon
name="hero-ellipsis-vertical"
class="-mt-1 h-5 w-5"
aria-hidden="true"
data-slot="icon"
/>
</button>
<.focus_wrap
id={"actions-#{@record.id}"}
class={[
"hidden pointer-events-auto absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white dark:bg-zinc-800 py-2 shadow-lg ring-1 ring-zinc-900/5 focus:outline-hidden"
]}
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu-0-button"
phx-click-away={close_actions_menu(@record.id)}
>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-edit"}
patch={~p"/collection/#{@record}/show/edit"}
>
<.icon
name="hero-pencil-square"
class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Edit")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-refresh-cover"}
phx-click={JS.push("refresh_cover", value: %{id: @record.id})}
>
<.icon
name="hero-photo"
class="h-4 w-4 mr-1 phx-click-loading:animate-bounce"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Refresh cover")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-refresh-mb-data"}
phx-click={JS.push("refresh_musicbrainz_data", value: %{id: @record.id})}
>
<.icon
name="hero-arrow-path"
class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Refresh MB data")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-populate-genres"}
phx-click={JS.push("populate_genres", value: %{id: @record.id})}
>
<.icon
name="hero-sparkles"
class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Populate genres")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-red-900 hover:bg-red-50 dark:text-red-700 dark:hover:bg-red-900/30 dark:hover:text-red-600"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-delete"}
phx-click={JS.push("delete", value: %{id: @record.id})}
data-confirm={gettext("Are you sure?")}
>
<.icon
name="hero-trash"
class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Delete")}
</.link>
</.focus_wrap>
</div>
</div>
<div class="mt-4">
<%!-- TODO: replace with OSS version --%>
<dl class="divide-y divide-zinc-100 dark:divide-slate-300/30">
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("ID")}
</dt>
<dd class="mt-1 text-xs md:text-sm flex justify-between leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
<code id={"record-#{@record.id}"}>{@record.id}</code>
<button phx-click={
JS.dispatch("music_library:clipcopy", to: "#record-" <> @record.id)
|> JS.transition("animate-shake")
}>
<span class="sr-only">{gettext("Copy record ID to clipboard")}</span>
<.icon
name="hero-clipboard-document"
class="-mt-1 h-5 w-5"
aria-hidden="true"
data-slot="icon"
/>
</button>
</dd>
</div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Genres")}
</dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
<.link <.link
:for={genre <- @record.genres} :for={artist <- @record.artists}
class="mr-2 text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300" class="text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
patch={~p"/collection?#{%{query: ~s(genre:"#{genre}")}}"} navigate={~p"/artists/#{artist.musicbrainz_id}"}
> >
{genre} {artist.name}
</.link> </.link>
</dd> </h1>
<h2 class="mt-1 flex font-semibold text-base sm:text-lg leading-5 text-zinc-700 dark:text-zinc-300 text-wrap">
{@record.title}
</h2>
<p class="mt-2 text-sm leading-5 text-zinc-500 dark:text-zinc-400">
{Records.Record.format_release_date(@record.release_date)} · {format_label(
@record.format
)} · {type_label(@record.type)}
</p>
</div> </div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> <div class="relative flex-none">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"> <button
{gettext("MusicBrainz ID")} type="button"
</dt> class="text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-300"
<dd class="mt-1 text-xs md:text-sm flex justify-between leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0"> aria-expanded="false"
<a href={MusicBrainz.ReleaseGroup.url(@record.musicbrainz_id)}> aria-haspopup="true"
<code id={"mb-#{@record.musicbrainz_id}"}>{@record.musicbrainz_id}</code> phx-click={toggle_actions_menu(@record.id)}
</a> >
<button phx-click={ <span class="sr-only">{gettext("Open options")}</span>
JS.dispatch("music_library:clipcopy", to: "#mb-" <> @record.musicbrainz_id) <.icon
|> JS.transition("animate-shake") name="hero-ellipsis-vertical"
}> class="-mt-1 h-5 w-5"
<span class="sr-only">{gettext("Copy MusicBrainz ID to clipboard")}</span> aria-hidden="true"
data-slot="icon"
/>
</button>
<.focus_wrap
id={"actions-#{@record.id}"}
class={[
"hidden pointer-events-auto absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white dark:bg-zinc-800 py-2 shadow-lg ring-1 ring-zinc-900/5 focus:outline-hidden"
]}
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu-0-button"
phx-click-away={close_actions_menu(@record.id)}
>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-edit"}
patch={~p"/collection/#{@record}/show/edit"}
>
<.icon <.icon
name="hero-clipboard-document" name="hero-pencil-square"
class="-mt-1 h-5 w-5" class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
aria-hidden="true" aria-hidden="true"
data-slot="icon" data-slot="icon"
/> />
</button> {gettext("Edit")}
</dd> </.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-refresh-cover"}
phx-click={JS.push("refresh_cover", value: %{id: @record.id})}
>
<.icon
name="hero-photo"
class="h-4 w-4 mr-1 phx-click-loading:animate-bounce"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Refresh cover")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-refresh-mb-data"}
phx-click={JS.push("refresh_musicbrainz_data", value: %{id: @record.id})}
>
<.icon
name="hero-arrow-path"
class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Refresh MB data")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-populate-genres"}
phx-click={JS.push("populate_genres", value: %{id: @record.id})}
>
<.icon
name="hero-sparkles"
class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Populate genres")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-red-900 hover:bg-red-50 dark:text-red-700 dark:hover:bg-red-900/30 dark:hover:text-red-600"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-delete"}
phx-click={JS.push("delete", value: %{id: @record.id})}
data-confirm={gettext("Are you sure?")}
>
<.icon
name="hero-trash"
class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Delete")}
</.link>
</.focus_wrap>
</div> </div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> </div>
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"> <div class="mt-4">
{gettext("Purchased on")} <%!-- TODO: replace with OSS version --%>
</dt> <dl class="divide-y divide-zinc-100 dark:divide-slate-300/30">
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0"> <div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
{Records.Record.format_as_date(@record.purchased_at)} <dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
</dd> {gettext("ID")}
</div> </dt>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> <dd class="mt-1 text-xs md:text-sm flex justify-between leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"> <code id={"record-#{@record.id}"}>{@record.id}</code>
{gettext("Published releases")} <button phx-click={
</dt> JS.dispatch("music_library:clipcopy", to: "#record-" <> @record.id)
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0"> |> JS.transition("animate-shake")
{Records.Record.release_count(@record)} }>
</dd> <span class="sr-only">{gettext("Copy record ID to clipboard")}</span>
</div> <.icon
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> name="hero-clipboard-document"
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"> class="-mt-1 h-5 w-5"
{gettext("Collected release")} aria-hidden="true"
</dt> data-slot="icon"
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0"> />
{selected_release_label(@record)} </button>
</dd> </dd>
</div> </div>
<div <div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
:if={Records.Record.included_release_groups_count(@record) > 0} <dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0" {gettext("Genres")}
> </dt>
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"> <dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
{gettext("Includes")} <.link
</dt> :for={genre <- @record.genres}
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0"> class="mr-2 text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
<ul> patch={~p"/collection?#{%{query: ~s(genre:"#{genre}")}}"}
<li :for={included_release_group <- Records.Record.included_release_groups(@record)}> >
{included_release_group.artists} - {included_release_group.title} {genre}
</li> </.link>
</ul> </dd>
</dd> </div>
</div> <div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> <dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"> {gettext("MusicBrainz ID")}
{gettext("Inserted at")} </dt>
</dt> <dd class="mt-1 text-xs md:text-sm flex justify-between leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0"> <a href={MusicBrainz.ReleaseGroup.url(@record.musicbrainz_id)}>
{Records.Record.format_as_date(@record.inserted_at)} <code id={"mb-#{@record.musicbrainz_id}"}>{@record.musicbrainz_id}</code>
</dd> </a>
</div> <button phx-click={
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> JS.dispatch("music_library:clipcopy", to: "#mb-" <> @record.musicbrainz_id)
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"> |> JS.transition("animate-shake")
{gettext("Updated at")} }>
</dt> <span class="sr-only">{gettext("Copy MusicBrainz ID to clipboard")}</span>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0"> <.icon
{Records.Record.format_as_date(@record.updated_at)} name="hero-clipboard-document"
</dd> class="-mt-1 h-5 w-5"
</div> aria-hidden="true"
</dl> data-slot="icon"
/>
</button>
</dd>
</div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Purchased on")}
</dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
{Records.Record.format_as_date(@record.purchased_at)}
</dd>
</div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Published releases")}
</dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
{Records.Record.release_count(@record)}
</dd>
</div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Collected release")}
</dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
{selected_release_label(@record)}
</dd>
</div>
<div
:if={Records.Record.included_release_groups_count(@record) > 0}
class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"
>
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Includes")}
</dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
<ul>
<li :for={included_release_group <- Records.Record.included_release_groups(@record)}>
{included_release_group.artists} - {included_release_group.title}
</li>
</ul>
</dd>
</div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Inserted at")}
</dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
{Records.Record.format_as_date(@record.inserted_at)}
</dd>
</div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Updated at")}
</dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
{Records.Record.format_as_date(@record.updated_at)}
</dd>
</div>
</dl>
</div>
</div> </div>
</div> </div>
<!-- TODO: extract to a component --> <!-- TODO: extract to a component -->
@@ -1,4 +1,4 @@
<div class="md:columns-2 mt-4 px-4"> <div class="md:flex mt-4 px-4 md:gap-x-4">
<div class="drop-shadow-sm md:max-w-[38rem]"> <div class="drop-shadow-sm md:max-w-[38rem]">
<img <img
class="w-full rounded-lg drop-shadow-sm" class="w-full rounded-lg drop-shadow-sm"
@@ -7,265 +7,267 @@
/> />
</div> </div>
<div class="mt-4 md:mt-0 flex justify-between"> <div class="grow">
<div> <div class="mt-4 md:mt-0 flex justify-between">
<h1 class="text-base font-medium leading-6 text-zinc-700"> <div>
<.link <h1 class="text-base font-medium leading-6 text-zinc-700">
:for={artist <- @record.artists}
class="text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
navigate={~p"/artists/#{artist.musicbrainz_id}"}
>
{artist.name}
</.link>
</h1>
<h2 class="mt-1 flex font-semibold text-base sm:text-lg leading-5 text-zinc-700 dark:text-zinc-300 text-wrap">
{@record.title}
</h2>
<p class="mt-2 text-sm leading-5 text-zinc-500 dark:text-zinc-400">
{Records.Record.format_release_date(@record.release_date)}
<span :if={@current_date && !Records.Record.released?(@record, @current_date)}>
({gettext("Unreleased")})
</span>
· {format_label(@record.format)} · {type_label(@record.type)}
</p>
</div>
<div class="relative flex-none">
<button
type="button"
class="text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-300"
aria-expanded="false"
aria-haspopup="true"
phx-click={toggle_actions_menu(@record.id)}
>
<span class="sr-only">{gettext("Open options")}</span>
<.icon
name="hero-ellipsis-vertical"
class="-mt-1 h-5 w-5"
aria-hidden="true"
data-slot="icon"
/>
</button>
<.focus_wrap
id={"actions-#{@record.id}"}
class={[
"hidden pointer-events-auto absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white dark:bg-zinc-800 py-2 shadow-lg ring-1 ring-zinc-900/5 focus:outline-hidden"
]}
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu-0-button"
phx-click-away={close_actions_menu(@record.id)}
>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-edit"}
patch={~p"/wishlist/#{@record}/show/edit"}
>
<.icon
name="hero-pencil-square"
class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Edit")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-refresh-cover"}
phx-click={JS.push("refresh_cover", value: %{id: @record.id})}
>
<.icon
name="hero-photo"
class="h-4 w-4 mr-1 phx-click-loading:animate-bounce"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Refresh cover")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-refresh-mb-data"}
phx-click={JS.push("refresh_musicbrainz_data", value: %{id: @record.id})}
>
<.icon
name="hero-arrow-path"
class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Refresh MB data")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-populate-genres"}
phx-click={JS.push("populate_genres", value: %{id: @record.id})}
>
<.icon
name="hero-sparkles"
class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Populate genres")}
</.link>
<.link
:if={!@record.purchased_at}
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-purchase"}
phx-click={
JS.dispatch("music_library:confetti")
|> JS.push("add-to-collection", value: %{id: @record.id})
}
>
<.icon
name="hero-banknotes"
class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Purchased")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-red-900 hover:bg-red-50 dark:text-red-700 dark:hover:bg-red-900/30 dark:hover:text-red-600"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-delete"}
phx-click={JS.push("delete", value: %{id: @record.id})}
data-confirm={gettext("Are you sure?")}
>
<.icon
name="hero-trash"
class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Delete")}
</.link>
</.focus_wrap>
</div>
</div>
<div class="mt-4">
<%!-- TODO: replace with OSS version --%>
<dl class="divide-y divide-zinc-100 dark:divide-slate-300/30">
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("ID")}
</dt>
<dd class="mt-1 text-xs md:text-sm flex justify-between leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
<code id={"record-#{@record.id}"}>{@record.id}</code>
<button phx-click={
JS.dispatch("music_library:clipcopy", to: "#record-" <> @record.id)
|> JS.transition("animate-shake")
}>
<span class="sr-only">{gettext("Copy record ID to clipboard")}</span>
<.icon
name="hero-clipboard-document"
class="-mt-1 h-5 w-5"
aria-hidden="true"
data-slot="icon"
/>
</button>
</dd>
</div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Genres")}
</dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
<.link <.link
:for={genre <- @record.genres} :for={artist <- @record.artists}
class="mr-2 text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300" class="text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
patch={~p"/wishlist?#{%{query: ~s(genre:"#{genre}")}}"} navigate={~p"/artists/#{artist.musicbrainz_id}"}
> >
{genre} {artist.name}
</.link> </.link>
</dd> </h1>
<h2 class="mt-1 flex font-semibold text-base sm:text-lg leading-5 text-zinc-700 dark:text-zinc-300 text-wrap">
{@record.title}
</h2>
<p class="mt-2 text-sm leading-5 text-zinc-500 dark:text-zinc-400">
{Records.Record.format_release_date(@record.release_date)}
<span :if={@current_date && !Records.Record.released?(@record, @current_date)}>
({gettext("Unreleased")})
</span>
· {format_label(@record.format)} · {type_label(@record.type)}
</p>
</div> </div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> <div class="relative flex-none">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"> <button
{gettext("MusicBrainz ID")} type="button"
</dt> class="text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-300"
<dd class="mt-1 text-xs md:text-sm flex justify-between leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0"> aria-expanded="false"
<a href={MusicBrainz.ReleaseGroup.url(@record.musicbrainz_id)}> aria-haspopup="true"
<code id={"mb-#{@record.musicbrainz_id}"}>{@record.musicbrainz_id}</code> phx-click={toggle_actions_menu(@record.id)}
</a> >
<button phx-click={ <span class="sr-only">{gettext("Open options")}</span>
JS.dispatch("music_library:clipcopy", to: "#mb-" <> @record.musicbrainz_id) <.icon
|> JS.transition("animate-shake") name="hero-ellipsis-vertical"
}> class="-mt-1 h-5 w-5"
<span class="sr-only">{gettext("Copy MusicBrainz ID to clipboard")}</span> aria-hidden="true"
data-slot="icon"
/>
</button>
<.focus_wrap
id={"actions-#{@record.id}"}
class={[
"hidden pointer-events-auto absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white dark:bg-zinc-800 py-2 shadow-lg ring-1 ring-zinc-900/5 focus:outline-hidden"
]}
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu-0-button"
phx-click-away={close_actions_menu(@record.id)}
>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-edit"}
patch={~p"/wishlist/#{@record}/show/edit"}
>
<.icon <.icon
name="hero-clipboard-document" name="hero-pencil-square"
class="-mt-1 h-5 w-5" class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
aria-hidden="true" aria-hidden="true"
data-slot="icon" data-slot="icon"
/> />
</button> {gettext("Edit")}
</dd> </.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-refresh-cover"}
phx-click={JS.push("refresh_cover", value: %{id: @record.id})}
>
<.icon
name="hero-photo"
class="h-4 w-4 mr-1 phx-click-loading:animate-bounce"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Refresh cover")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-refresh-mb-data"}
phx-click={JS.push("refresh_musicbrainz_data", value: %{id: @record.id})}
>
<.icon
name="hero-arrow-path"
class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Refresh MB data")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-populate-genres"}
phx-click={JS.push("populate_genres", value: %{id: @record.id})}
>
<.icon
name="hero-sparkles"
class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Populate genres")}
</.link>
<.link
:if={!@record.purchased_at}
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-purchase"}
phx-click={
JS.dispatch("music_library:confetti")
|> JS.push("add-to-collection", value: %{id: @record.id})
}
>
<.icon
name="hero-banknotes"
class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Purchased")}
</.link>
<.link
class="block px-3 py-1 text-sm leading-6 text-red-900 hover:bg-red-50 dark:text-red-700 dark:hover:bg-red-900/30 dark:hover:text-red-600"
role="menuitem"
tabindex="0"
id={"actions-#{@record.id}-delete"}
phx-click={JS.push("delete", value: %{id: @record.id})}
data-confirm={gettext("Are you sure?")}
>
<.icon
name="hero-trash"
class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
aria-hidden="true"
data-slot="icon"
/>
{gettext("Delete")}
</.link>
</.focus_wrap>
</div> </div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> </div>
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Published releases")} <div class="mt-4">
</dt> <%!-- TODO: replace with OSS version --%>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0"> <dl class="divide-y divide-zinc-100 dark:divide-slate-300/30">
{Records.Record.release_count(@record)} <div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
</dd> <dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
</div> {gettext("ID")}
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> </dt>
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"> <dd class="mt-1 text-xs md:text-sm flex justify-between leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
{gettext("Collected release")} <code id={"record-#{@record.id}"}>{@record.id}</code>
</dt> <button phx-click={
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0"> JS.dispatch("music_library:clipcopy", to: "#record-" <> @record.id)
{selected_release_label(@record)} |> JS.transition("animate-shake")
</dd> }>
</div> <span class="sr-only">{gettext("Copy record ID to clipboard")}</span>
<div <.icon
:if={Records.Record.included_release_groups_count(@record) > 0} name="hero-clipboard-document"
class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0" class="-mt-1 h-5 w-5"
> aria-hidden="true"
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"> data-slot="icon"
{gettext("Includes")} />
</dt> </button>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0"> </dd>
<ul> </div>
<li :for={included_release_group <- Records.Record.included_release_groups(@record)}> <div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
{included_release_group.artists} - {included_release_group.title} <dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
</li> {gettext("Genres")}
</ul> </dt>
</dd> <dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
</div> <.link
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> :for={genre <- @record.genres}
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"> class="mr-2 text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
{gettext("Inserted at")} patch={~p"/wishlist?#{%{query: ~s(genre:"#{genre}")}}"}
</dt> >
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0"> {genre}
{Records.Record.format_as_date(@record.inserted_at)} </.link>
</dd> </dd>
</div> </div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> <div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"> <dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Updated at")} {gettext("MusicBrainz ID")}
</dt> </dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0"> <dd class="mt-1 text-xs md:text-sm flex justify-between leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
{Records.Record.format_as_date(@record.updated_at)} <a href={MusicBrainz.ReleaseGroup.url(@record.musicbrainz_id)}>
</dd> <code id={"mb-#{@record.musicbrainz_id}"}>{@record.musicbrainz_id}</code>
</div> </a>
</dl> <button phx-click={
JS.dispatch("music_library:clipcopy", to: "#mb-" <> @record.musicbrainz_id)
|> JS.transition("animate-shake")
}>
<span class="sr-only">{gettext("Copy MusicBrainz ID to clipboard")}</span>
<.icon
name="hero-clipboard-document"
class="-mt-1 h-5 w-5"
aria-hidden="true"
data-slot="icon"
/>
</button>
</dd>
</div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Published releases")}
</dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
{Records.Record.release_count(@record)}
</dd>
</div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Collected release")}
</dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
{selected_release_label(@record)}
</dd>
</div>
<div
:if={Records.Record.included_release_groups_count(@record) > 0}
class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"
>
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Includes")}
</dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
<ul>
<li :for={included_release_group <- Records.Record.included_release_groups(@record)}>
{included_release_group.artists} - {included_release_group.title}
</li>
</ul>
</dd>
</div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Inserted at")}
</dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
{Records.Record.format_as_date(@record.inserted_at)}
</dd>
</div>
<div class="py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-xs md:text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
{gettext("Updated at")}
</dt>
<dd class="mt-1 text-xs md:text-sm leading-6 text-zinc-700 dark:text-zinc-300 sm:col-span-2 sm:mt-0">
{Records.Record.format_as_date(@record.updated_at)}
</dd>
</div>
</dl>
</div>
</div> </div>
</div> </div>
<!-- TODO: extract to a component --> <!-- TODO: extract to a component -->