diff --git a/lib/music_library/records.ex b/lib/music_library/records.ex
new file mode 100644
index 00000000..1dc5e7ca
--- /dev/null
+++ b/lib/music_library/records.ex
@@ -0,0 +1,104 @@
+defmodule MusicLibrary.Records do
+ @moduledoc """
+ The Records context.
+ """
+
+ import Ecto.Query, warn: false
+ alias MusicLibrary.Repo
+
+ alias MusicLibrary.Records.Record
+
+ @doc """
+ Returns the list of records.
+
+ ## Examples
+
+ iex> list_records()
+ [%Record{}, ...]
+
+ """
+ def list_records do
+ Repo.all(Record)
+ end
+
+ @doc """
+ Gets a single record.
+
+ Raises `Ecto.NoResultsError` if the Record does not exist.
+
+ ## Examples
+
+ iex> get_record!(123)
+ %Record{}
+
+ iex> get_record!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_record!(id), do: Repo.get!(Record, id)
+
+ @doc """
+ Creates a record.
+
+ ## Examples
+
+ iex> create_record(%{field: value})
+ {:ok, %Record{}}
+
+ iex> create_record(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_record(attrs \\ %{}) do
+ %Record{}
+ |> Record.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a record.
+
+ ## Examples
+
+ iex> update_record(record, %{field: new_value})
+ {:ok, %Record{}}
+
+ iex> update_record(record, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_record(%Record{} = record, attrs) do
+ record
+ |> Record.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a record.
+
+ ## Examples
+
+ iex> delete_record(record)
+ {:ok, %Record{}}
+
+ iex> delete_record(record)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_record(%Record{} = record) do
+ Repo.delete(record)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking record changes.
+
+ ## Examples
+
+ iex> change_record(record)
+ %Ecto.Changeset{data: %Record{}}
+
+ """
+ def change_record(%Record{} = record, attrs \\ %{}) do
+ Record.changeset(record, attrs)
+ end
+end
diff --git a/lib/music_library/records/record.ex b/lib/music_library/records/record.ex
new file mode 100644
index 00000000..dc9cc3f3
--- /dev/null
+++ b/lib/music_library/records/record.ex
@@ -0,0 +1,24 @@
+defmodule MusicLibrary.Records.Record do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ schema "records" do
+ field :type, Ecto.Enum, values: [:album, :ep, :live, :compilation, :single, :other]
+ field :title, :string
+ field :image, :string
+ field :year, :integer
+ field :musicbrainz_id, Ecto.UUID
+ field :genres, {:array, :string}
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(record, attrs) do
+ record
+ |> cast(attrs, [:type, :title, :musicbrainz_id, :year, :genres, :image])
+ |> validate_required([:type, :title, :musicbrainz_id, :year, :genres, :image])
+ end
+end
diff --git a/lib/music_library_web/live/record_live/form_component.ex b/lib/music_library_web/live/record_live/form_component.ex
new file mode 100644
index 00000000..84123f1c
--- /dev/null
+++ b/lib/music_library_web/live/record_live/form_component.ex
@@ -0,0 +1,99 @@
+defmodule MusicLibraryWeb.RecordLive.FormComponent do
+ use MusicLibraryWeb, :live_component
+
+ alias MusicLibrary.Records
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.header>
+ <%= @title %>
+ <:subtitle>Use this form to manage record records in your database.
+
+
+ <.simple_form
+ for={@form}
+ id="record-form"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ >
+ <.input
+ field={@form[:type]}
+ type="select"
+ label="Type"
+ prompt="Choose a value"
+ options={Ecto.Enum.values(MusicLibrary.Records.Record, :type)}
+ />
+ <.input field={@form[:title]} type="text" label="Title" />
+ <.input field={@form[:musicbrainz_id]} type="text" label="Musicbrainz" />
+ <.input field={@form[:year]} type="number" label="Year" />
+ <.input
+ field={@form[:genres]}
+ type="select"
+ multiple
+ label="Genres"
+ options={[{"Option 1", "option1"}, {"Option 2", "option2"}]}
+ />
+ <.input field={@form[:image]} type="text" label="Image" />
+ <:actions>
+ <.button phx-disable-with="Saving...">Save Record
+
+
+
+ """
+ end
+
+ @impl true
+ def update(%{record: record} = assigns, socket) do
+ {:ok,
+ socket
+ |> assign(assigns)
+ |> assign_new(:form, fn ->
+ to_form(Records.change_record(record))
+ end)}
+ end
+
+ @impl true
+ def handle_event("validate", %{"record" => record_params}, socket) do
+ changeset = Records.change_record(socket.assigns.record, record_params)
+ {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
+ end
+
+ def handle_event("save", %{"record" => record_params}, socket) do
+ save_record(socket, socket.assigns.action, record_params)
+ end
+
+ defp save_record(socket, :edit, record_params) do
+ case Records.update_record(socket.assigns.record, record_params) do
+ {:ok, record} ->
+ notify_parent({:saved, record})
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "Record updated successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp save_record(socket, :new, record_params) do
+ case Records.create_record(record_params) do
+ {:ok, record} ->
+ notify_parent({:saved, record})
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "Record created successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
+end
diff --git a/lib/music_library_web/live/record_live/index.ex b/lib/music_library_web/live/record_live/index.ex
new file mode 100644
index 00000000..11606ebc
--- /dev/null
+++ b/lib/music_library_web/live/record_live/index.ex
@@ -0,0 +1,47 @@
+defmodule MusicLibraryWeb.RecordLive.Index do
+ use MusicLibraryWeb, :live_view
+
+ alias MusicLibrary.Records
+ alias MusicLibrary.Records.Record
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok, stream(socket, :records, Records.list_records())}
+ end
+
+ @impl true
+ def handle_params(params, _url, socket) do
+ {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ end
+
+ defp apply_action(socket, :edit, %{"id" => id}) do
+ socket
+ |> assign(:page_title, "Edit Record")
+ |> assign(:record, Records.get_record!(id))
+ end
+
+ defp apply_action(socket, :new, _params) do
+ socket
+ |> assign(:page_title, "New Record")
+ |> assign(:record, %Record{})
+ end
+
+ defp apply_action(socket, :index, _params) do
+ socket
+ |> assign(:page_title, "Listing Records")
+ |> assign(:record, nil)
+ end
+
+ @impl true
+ def handle_info({MusicLibraryWeb.RecordLive.FormComponent, {:saved, record}}, socket) do
+ {:noreply, stream_insert(socket, :records, record)}
+ end
+
+ @impl true
+ def handle_event("delete", %{"id" => id}, socket) do
+ record = Records.get_record!(id)
+ {:ok, _} = Records.delete_record(record)
+
+ {:noreply, stream_delete(socket, :records, record)}
+ end
+end
diff --git a/lib/music_library_web/live/record_live/index.html.heex b/lib/music_library_web/live/record_live/index.html.heex
new file mode 100644
index 00000000..17fadfed
--- /dev/null
+++ b/lib/music_library_web/live/record_live/index.html.heex
@@ -0,0 +1,46 @@
+<.header>
+ Listing Records
+ <:actions>
+ <.link patch={~p"/records/new"}>
+ <.button>New Record
+
+
+
+
+<.table
+ id="records"
+ rows={@streams.records}
+ row_click={fn {_id, record} -> JS.navigate(~p"/records/#{record}") end}
+>
+ <:col :let={{_id, record}} label="Type"><%= record.type %>
+ <:col :let={{_id, record}} label="Title"><%= record.title %>
+ <:col :let={{_id, record}} label="Musicbrainz"><%= record.musicbrainz_id %>
+ <:col :let={{_id, record}} label="Year"><%= record.year %>
+ <:col :let={{_id, record}} label="Genres"><%= record.genres %>
+ <:col :let={{_id, record}} label="Image"><%= record.image %>
+ <:action :let={{_id, record}}>
+
+ <.link navigate={~p"/records/#{record}"}>Show
+
+ <.link patch={~p"/records/#{record}/edit"}>Edit
+
+ <:action :let={{id, record}}>
+ <.link
+ phx-click={JS.push("delete", value: %{id: record.id}) |> hide("##{id}")}
+ data-confirm="Are you sure?"
+ >
+ Delete
+
+
+
+
+<.modal :if={@live_action in [:new, :edit]} id="record-modal" show on_cancel={JS.patch(~p"/records")}>
+ <.live_component
+ module={MusicLibraryWeb.RecordLive.FormComponent}
+ id={@record.id || :new}
+ title={@page_title}
+ action={@live_action}
+ record={@record}
+ patch={~p"/records"}
+ />
+
diff --git a/lib/music_library_web/live/record_live/show.ex b/lib/music_library_web/live/record_live/show.ex
new file mode 100644
index 00000000..29228d4a
--- /dev/null
+++ b/lib/music_library_web/live/record_live/show.ex
@@ -0,0 +1,21 @@
+defmodule MusicLibraryWeb.RecordLive.Show do
+ use MusicLibraryWeb, :live_view
+
+ alias MusicLibrary.Records
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok, socket}
+ end
+
+ @impl true
+ def handle_params(%{"id" => id}, _, socket) do
+ {:noreply,
+ socket
+ |> assign(:page_title, page_title(socket.assigns.live_action))
+ |> assign(:record, Records.get_record!(id))}
+ end
+
+ defp page_title(:show), do: "Show Record"
+ defp page_title(:edit), do: "Edit Record"
+end
diff --git a/lib/music_library_web/live/record_live/show.html.heex b/lib/music_library_web/live/record_live/show.html.heex
new file mode 100644
index 00000000..481f198b
--- /dev/null
+++ b/lib/music_library_web/live/record_live/show.html.heex
@@ -0,0 +1,31 @@
+<.header>
+ Record <%= @record.id %>
+ <:subtitle>This is a record record from your database.
+ <:actions>
+ <.link patch={~p"/records/#{@record}/show/edit"} phx-click={JS.push_focus()}>
+ <.button>Edit record
+
+
+
+
+<.list>
+ <:item title="Type"><%= @record.type %>
+ <:item title="Title"><%= @record.title %>
+ <:item title="Musicbrainz"><%= @record.musicbrainz_id %>
+ <:item title="Year"><%= @record.year %>
+ <:item title="Genres"><%= @record.genres %>
+ <:item title="Image"><%= @record.image %>
+
+
+<.back navigate={~p"/records"}>Back to records
+
+<.modal :if={@live_action == :edit} id="record-modal" show on_cancel={JS.patch(~p"/records/#{@record}")}>
+ <.live_component
+ module={MusicLibraryWeb.RecordLive.FormComponent}
+ id={@record.id}
+ title={@page_title}
+ action={@live_action}
+ record={@record}
+ patch={~p"/records/#{@record}"}
+ />
+
diff --git a/lib/music_library_web/router.ex b/lib/music_library_web/router.ex
index ebf47fb2..5a42eccd 100644
--- a/lib/music_library_web/router.ex
+++ b/lib/music_library_web/router.ex
@@ -18,6 +18,13 @@ defmodule MusicLibraryWeb.Router do
pipe_through :browser
get "/", PageController, :home
+
+ live "/records", RecordLive.Index, :index
+ live "/records/new", RecordLive.Index, :new
+ live "/records/:id/edit", RecordLive.Index, :edit
+
+ live "/records/:id", RecordLive.Show, :show
+ live "/records/:id/show/edit", RecordLive.Show, :edit
end
# Other scopes may use custom stacks.
diff --git a/priv/repo/migrations/20240912134006_create_records.exs b/priv/repo/migrations/20240912134006_create_records.exs
new file mode 100644
index 00000000..2eb6c4a2
--- /dev/null
+++ b/priv/repo/migrations/20240912134006_create_records.exs
@@ -0,0 +1,17 @@
+defmodule MusicLibrary.Repo.Migrations.CreateRecords do
+ use Ecto.Migration
+
+ def change do
+ create table(:records, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+ add :type, :string
+ add :title, :string
+ add :musicbrainz_id, :uuid
+ add :year, :integer
+ add :genres, {:array, :string}
+ add :image, :string
+
+ timestamps(type: :utc_datetime)
+ end
+ end
+end
diff --git a/test/music_library/records_test.exs b/test/music_library/records_test.exs
new file mode 100644
index 00000000..38a86bc7
--- /dev/null
+++ b/test/music_library/records_test.exs
@@ -0,0 +1,69 @@
+defmodule MusicLibrary.RecordsTest do
+ use MusicLibrary.DataCase
+
+ alias MusicLibrary.Records
+
+ describe "records" do
+ alias MusicLibrary.Records.Record
+
+ import MusicLibrary.RecordsFixtures
+
+ @invalid_attrs %{type: nil, title: nil, image: nil, year: nil, musicbrainz_id: nil, genres: nil}
+
+ test "list_records/0 returns all records" do
+ record = record_fixture()
+ assert Records.list_records() == [record]
+ end
+
+ test "get_record!/1 returns the record with given id" do
+ record = record_fixture()
+ assert Records.get_record!(record.id) == record
+ end
+
+ test "create_record/1 with valid data creates a record" do
+ valid_attrs = %{type: :album, title: "some title", image: "some image", year: 42, musicbrainz_id: "7488a646-e31f-11e4-aace-600308960662", genres: ["option1", "option2"]}
+
+ assert {:ok, %Record{} = record} = Records.create_record(valid_attrs)
+ assert record.type == :album
+ assert record.title == "some title"
+ assert record.image == "some image"
+ assert record.year == 42
+ assert record.musicbrainz_id == "7488a646-e31f-11e4-aace-600308960662"
+ assert record.genres == ["option1", "option2"]
+ end
+
+ test "create_record/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Records.create_record(@invalid_attrs)
+ end
+
+ test "update_record/2 with valid data updates the record" do
+ record = record_fixture()
+ update_attrs = %{type: :ep, title: "some updated title", image: "some updated image", year: 43, musicbrainz_id: "7488a646-e31f-11e4-aace-600308960668", genres: ["option1"]}
+
+ assert {:ok, %Record{} = record} = Records.update_record(record, update_attrs)
+ assert record.type == :ep
+ assert record.title == "some updated title"
+ assert record.image == "some updated image"
+ assert record.year == 43
+ assert record.musicbrainz_id == "7488a646-e31f-11e4-aace-600308960668"
+ assert record.genres == ["option1"]
+ end
+
+ test "update_record/2 with invalid data returns error changeset" do
+ record = record_fixture()
+ assert {:error, %Ecto.Changeset{}} = Records.update_record(record, @invalid_attrs)
+ assert record == Records.get_record!(record.id)
+ end
+
+ test "delete_record/1 deletes the record" do
+ record = record_fixture()
+ assert {:ok, %Record{}} = Records.delete_record(record)
+ assert_raise Ecto.NoResultsError, fn -> Records.get_record!(record.id) end
+ end
+
+ test "change_record/1 returns a record changeset" do
+ record = record_fixture()
+ assert %Ecto.Changeset{} = Records.change_record(record)
+ end
+ end
+end
diff --git a/test/music_library_web/live/record_live_test.exs b/test/music_library_web/live/record_live_test.exs
new file mode 100644
index 00000000..fe2d8a15
--- /dev/null
+++ b/test/music_library_web/live/record_live_test.exs
@@ -0,0 +1,113 @@
+defmodule MusicLibraryWeb.RecordLiveTest do
+ use MusicLibraryWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+ import MusicLibrary.RecordsFixtures
+
+ @create_attrs %{type: :album, title: "some title", image: "some image", year: 42, musicbrainz_id: "7488a646-e31f-11e4-aace-600308960662", genres: ["option1", "option2"]}
+ @update_attrs %{type: :ep, title: "some updated title", image: "some updated image", year: 43, musicbrainz_id: "7488a646-e31f-11e4-aace-600308960668", genres: ["option1"]}
+ @invalid_attrs %{type: nil, title: nil, image: nil, year: nil, musicbrainz_id: nil, genres: []}
+
+ defp create_record(_) do
+ record = record_fixture()
+ %{record: record}
+ end
+
+ describe "Index" do
+ setup [:create_record]
+
+ test "lists all records", %{conn: conn, record: record} do
+ {:ok, _index_live, html} = live(conn, ~p"/records")
+
+ assert html =~ "Listing Records"
+ assert html =~ record.title
+ end
+
+ test "saves new record", %{conn: conn} do
+ {:ok, index_live, _html} = live(conn, ~p"/records")
+
+ assert index_live |> element("a", "New Record") |> render_click() =~
+ "New Record"
+
+ assert_patch(index_live, ~p"/records/new")
+
+ assert index_live
+ |> form("#record-form", record: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert index_live
+ |> form("#record-form", record: @create_attrs)
+ |> render_submit()
+
+ assert_patch(index_live, ~p"/records")
+
+ html = render(index_live)
+ assert html =~ "Record created successfully"
+ assert html =~ "some title"
+ end
+
+ test "updates record in listing", %{conn: conn, record: record} do
+ {:ok, index_live, _html} = live(conn, ~p"/records")
+
+ assert index_live |> element("#records-#{record.id} a", "Edit") |> render_click() =~
+ "Edit Record"
+
+ assert_patch(index_live, ~p"/records/#{record}/edit")
+
+ assert index_live
+ |> form("#record-form", record: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert index_live
+ |> form("#record-form", record: @update_attrs)
+ |> render_submit()
+
+ assert_patch(index_live, ~p"/records")
+
+ html = render(index_live)
+ assert html =~ "Record updated successfully"
+ assert html =~ "some updated title"
+ end
+
+ test "deletes record in listing", %{conn: conn, record: record} do
+ {:ok, index_live, _html} = live(conn, ~p"/records")
+
+ assert index_live |> element("#records-#{record.id} a", "Delete") |> render_click()
+ refute has_element?(index_live, "#records-#{record.id}")
+ end
+ end
+
+ describe "Show" do
+ setup [:create_record]
+
+ test "displays record", %{conn: conn, record: record} do
+ {:ok, _show_live, html} = live(conn, ~p"/records/#{record}")
+
+ assert html =~ "Show Record"
+ assert html =~ record.title
+ end
+
+ test "updates record within modal", %{conn: conn, record: record} do
+ {:ok, show_live, _html} = live(conn, ~p"/records/#{record}")
+
+ assert show_live |> element("a", "Edit") |> render_click() =~
+ "Edit Record"
+
+ assert_patch(show_live, ~p"/records/#{record}/show/edit")
+
+ assert show_live
+ |> form("#record-form", record: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert show_live
+ |> form("#record-form", record: @update_attrs)
+ |> render_submit()
+
+ assert_patch(show_live, ~p"/records/#{record}")
+
+ html = render(show_live)
+ assert html =~ "Record updated successfully"
+ assert html =~ "some updated title"
+ end
+ end
+end
diff --git a/test/support/fixtures/records_fixtures.ex b/test/support/fixtures/records_fixtures.ex
new file mode 100644
index 00000000..0f8d9f17
--- /dev/null
+++ b/test/support/fixtures/records_fixtures.ex
@@ -0,0 +1,25 @@
+defmodule MusicLibrary.RecordsFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `MusicLibrary.Records` context.
+ """
+
+ @doc """
+ Generate a record.
+ """
+ def record_fixture(attrs \\ %{}) do
+ {:ok, record} =
+ attrs
+ |> Enum.into(%{
+ genres: ["option1", "option2"],
+ image: "some image",
+ musicbrainz_id: "7488a646-e31f-11e4-aace-600308960662",
+ title: "some title",
+ type: :album,
+ year: 42
+ })
+ |> MusicLibrary.Records.create_record()
+
+ record
+ end
+end