# Online Store Quick Check System - Implementation Plan ## Overview This plan outlines the implementation of a system to quickly check for the presence of wishlisted records on configurable online record stores. The system will add an expandable section to record details pages with links to online stores, generated from user-configurable templates. ## Requirements Analysis ### Core Features - **Expandable section on record details**: Collapsed by default, shows links to online stores - **Configurable store templates**: User-managed templates stored in database - **Template variables**: Support for record artist and title as search parameters - **User management interface**: Dedicated section for managing store templates ### Example Use Case Create a template for Amazon UK search: `https://www.amazon.co.uk/s?k={artist}+{title}+vinyl` When viewing a record by "Pink Floyd" with title "The Wall", generate: `https://www.amazon.co.uk/s?k=Pink+Floyd+The+Wall+vinyl` ## Database Schema Design ### Online Store Templates Table Following the application's established patterns: ```elixir # Migration: create_online_store_templates.exs defmodule MusicLibrary.Repo.Migrations.CreateOnlineStoreTemplates do use Ecto.Migration def change do create table(:online_store_templates, primary_key: false) do add :id, :binary_id, primary_key: true add :name, :string, null: false add :description, :text add :url_template, :text, null: false add :enabled, :boolean, default: true, null: false timestamps(type: :utc_datetime) end create index(:online_store_templates, [:enabled]) end end ``` ### Schema Module ```elixir # lib/music_library/online_store_templates/online_store_template.ex defmodule MusicLibrary.OnlineStoreTemplates.OnlineStoreTemplate do use Ecto.Schema import Ecto.Changeset @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "online_store_templates" do field :name, :string field :description, :string field :url_template, :string field :enabled, :boolean, default: true timestamps(type: :utc_datetime) end @doc false def changeset(template, attrs) do template |> cast(attrs, [:name, :description, :url_template, :enabled]) |> validate_required([:name, :url_template]) |> validate_length(:name, min: 1, max: 100) |> validate_length(:url_template, min: 1, max: 500) |> validate_url_template() end defp validate_url_template(changeset) do if template = get_field(changeset, :url_template) do case URI.parse(template) do %URI{scheme: scheme} when scheme in ["http", "https"] -> changeset _ -> add_error(changeset, :url_template, "must be a valid HTTP or HTTPS URL") end else changeset end end end ``` ### Context Module ```elixir # lib/music_library/online_store_templates.ex defmodule MusicLibrary.OnlineStoreTemplates do @moduledoc """ The OnlineStoreTemplates context. """ import Ecto.Query, warn: false alias MusicLibrary.Repo alias MusicLibrary.OnlineStoreTemplates.OnlineStoreTemplate @doc """ Returns the list of enabled online store templates ordered by name. """ def list_enabled_templates do OnlineStoreTemplate |> where([t], t.enabled == true) |> order_by([t], [asc: t.name]) |> Repo.all() end @doc """ Returns the list of all online store templates for management. """ def list_templates do OnlineStoreTemplate |> order_by([t], [asc: t.name]) |> Repo.all() end @doc """ Gets a single online store template. """ def get_template!(id), do: Repo.get!(OnlineStoreTemplate, id) @doc """ Creates an online store template. """ def create_template(attrs \\ %{}) do %OnlineStoreTemplate{} |> OnlineStoreTemplate.changeset(attrs) |> Repo.insert() end @doc """ Updates an online store template. """ def update_template(%OnlineStoreTemplate{} = template, attrs) do template |> OnlineStoreTemplate.changeset(attrs) |> Repo.update() end @doc """ Deletes an online store template. """ def delete_template(%OnlineStoreTemplate{} = template) do Repo.delete(template) end @doc """ Returns an `%Ecto.Changeset{}` for tracking online store template changes. """ def change_template(%OnlineStoreTemplate{} = template, attrs \\ %{}) do OnlineStoreTemplate.changeset(template, attrs) end @doc """ Generates a URL from a template by replacing variables with record data. """ def generate_url(template, record) do artists_string = record.artists |> Enum.map(& &1.name) |> Enum.join(" ") template.url_template |> String.replace("{artist}", URI.encode(artists_string)) |> String.replace("{title}", URI.encode(record.title)) end end ``` ## User Interface Design ### Management Interface Following the Scrobble Rules pattern for consistency: #### Route Addition ```elixir # lib/music_library_web/router.ex # Add to the authenticated scope: live "/online-store-templates", OnlineStoreTemplateLive.Index, :index live "/online-store-templates/new", OnlineStoreTemplateLive.Index, :new live "/online-store-templates/:id/edit", OnlineStoreTemplateLive.Index, :edit ``` #### Navigation Integration ```elixir # lib/music_library_web/components/layouts/app.html.heex # Add to the "More" dropdown menu: <.dropdown_link href={~p"/online-store-templates"}> Online Store Templates ``` #### LiveView Index Page ```elixir # lib/music_library_web/live/online_store_template_live/index.ex defmodule MusicLibraryWeb.OnlineStoreTemplateLive.Index do use MusicLibraryWeb, :live_view alias MusicLibrary.OnlineStoreTemplates alias MusicLibrary.OnlineStoreTemplates.OnlineStoreTemplate @impl true def mount(_params, _session, socket) do {:ok, stream(socket, :templates, OnlineStoreTemplates.list_templates())} 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 Online Store Template") |> assign(:template, OnlineStoreTemplates.get_template!(id)) end defp apply_action(socket, :new, _params) do socket |> assign(:page_title, "New Online Store Template") |> assign(:template, %OnlineStoreTemplate{}) end defp apply_action(socket, :index, _params) do socket |> assign(:page_title, "Online Store Templates") |> assign(:template, nil) end @impl true def handle_info({MusicLibraryWeb.OnlineStoreTemplateLive.FormComponent, {:saved, template}}, socket) do {:noreply, stream_insert(socket, :templates, template)} end @impl true def handle_event("delete", %{"id" => id}, socket) do template = OnlineStoreTemplates.get_template!(id) {:ok, _} = OnlineStoreTemplates.delete_template(template) {:noreply, stream_delete(socket, :templates, template)} end @impl true def handle_event("toggle-enabled", %{"id" => id}, socket) do template = OnlineStoreTemplates.get_template!(id) {:ok, updated_template} = OnlineStoreTemplates.update_template(template, %{enabled: !template.enabled}) {:noreply, stream_insert(socket, :templates, updated_template)} end end ``` #### Template Design ```heex
Manage templates for generating links to online record stores.
<%= template.name %>
<.badge :if={template.enabled} color="success" size="sm"> Enabled <.badge :if={!template.enabled} color="warning" size="sm"> Disabled<%= template.url_template %>
<%= template.description %>