Files
2026-05-21 07:49:36 +01:00

169 lines
4.7 KiB
Elixir

defmodule MusicLibraryWeb.Components.Notes do
use MusicLibraryWeb, :live_component
alias MusicLibrary.Notes
alias MusicLibrary.Notes.Note
alias MusicLibraryWeb.Markdown
def open(id), do: Fluxon.open_dialog(id)
@impl true
def update(assigns, socket) do
note = find_or_initialize_note(assigns)
changeset =
Notes.change_note(note, %{})
{:ok,
socket
|> assign(assigns)
|> assign(:note, note)
|> assign(:mode, initial_mode(changeset))
|> assign(:form, to_form(changeset))}
end
defp find_or_initialize_note(%{entity: entity, musicbrainz_id: musicbrainz_id}) do
case Notes.get_note(entity, musicbrainz_id) do
nil ->
%Note{entity: entity, musicbrainz_id: musicbrainz_id}
note ->
note
end
end
@impl true
def render(assigns) do
~H"""
<div>
<.sheet
id={@sheet_id}
placement="right"
class="w-md py-16 sm:min-w-lg lg:min-w-2xl"
>
<.tabs>
<.tabs_list variant="segmented" class="mx-auto w-48 md:mx-0" active_tab={@mode}>
<:tab name="read" phx-click="set_mode" phx-value-mode="read" phx-target={@myself}>
{gettext("Read")}
</:tab>
<:tab name="edit" phx-click="set_mode" phx-value-mode="edit" phx-target={@myself}>
{gettext("Edit")}
</:tab>
</.tabs_list>
<.tabs_panel active={@mode == "read"} name="read">
<article class="dark:prose-invert prose prose-h1:text-sm prose-sm prose-zinc mt-5 w-full">
{render_content(@form[:content].value)}
</article>
</.tabs_panel>
<.tabs_panel active={@mode == "edit"} name="edit">
<.simple_form
for={@form}
id="notes-form"
phx-target={@myself}
phx-change="validate"
phx-auto-recover="recover_form"
phx-submit="save"
>
<.textarea
class={[
"min-h-128 w-full overflow-scroll font-mono md:min-h-164",
@form[:content].value != @note.content &&
"border-amber-300 focus-visible:border-amber-300"
]}
field={@form[:content]}
/>
<:actions>
<div class="w-full md:flex md:justify-center">
<.button
variant="solid"
class="w-full md:w-auto"
phx-disable-with={gettext("Saving...")}
>
{gettext("Save")}
</.button>
</div>
</:actions>
</.simple_form>
</.tabs_panel>
</.tabs>
</.sheet>
</div>
"""
end
@impl true
def handle_event("validate", %{"note" => note_params}, socket) do
changeset = Notes.change_note(socket.assigns.note, note_params)
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
end
def handle_event("save", %{"note" => note_params}, socket) do
if socket.assigns.note.id do
update_note(note_params, socket)
else
create_note(note_params, socket)
end
end
def handle_event("recover_form", params, socket) do
handle_event("validate", params, socket)
end
def handle_event("set_mode", %{"mode" => mode}, socket) when mode in ["read", "edit"] do
{:noreply, assign(socket, :mode, mode)}
end
defp update_note(note_params, socket) do
case Notes.update_note(socket.assigns.note, note_params) do
{:ok, note} ->
put_toast!(:info, gettext("Note updated successfully"))
changeset =
Notes.change_note(note, %{})
{:noreply,
socket
|> assign(:note, note)
|> assign(:form, to_form(changeset))}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp create_note(note_params, socket) do
case Notes.create_note(socket.assigns.note, note_params) do
{:ok, note} ->
put_toast!(:info, gettext("Note created successfully"))
changeset =
Notes.change_note(note, %{})
{:noreply,
socket
|> assign(:note, note)
|> assign(:form, to_form(changeset))}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp initial_mode(changeset) do
if Ecto.Changeset.get_field(changeset, :content) in [nil, ""] do
"edit"
else
"read"
end
end
# sobelow_skip ["XSS.Raw"]
# Markdown.to_html/1 sanitizes HTML via MDEx (ammonia)
defp render_content(content) do
content
|> Markdown.to_html()
|> raw()
end
end