defmodule AmmopricesWeb.CaliberLive.Show do @moduledoc false use AmmopricesWeb, :live_view import AmmopricesWeb.PriceComponents alias Ammoprices.Catalog alias Ammoprices.Prices @range_days %{ "7d" => 7, "30d" => 30, "90d" => 90, "1y" => 365, "all" => 3650 } @impl true def mount(%{"slug" => slug}, _session, socket) do caliber = Catalog.get_caliber_by_slug!(slug) if connected?(socket) do Phoenix.PubSub.subscribe(Ammoprices.PubSub, "prices:updated") end socket = socket |> assign( page_title: caliber.name, caliber: caliber, in_stock_filter: true, casing_filter: nil, grain_weight_filter: nil, subsonic_filter: nil, chart_range: "30d" ) |> load_filter_options() |> load_products() |> load_chart_data() |> load_price_stats() {:ok, socket} end @impl true def handle_event("toggle_stock_filter", _params, socket) do socket = socket |> assign(:in_stock_filter, !socket.assigns.in_stock_filter) |> load_products() {:noreply, socket} end @impl true def handle_event("filter_casing", %{"casing" => casing}, socket) do current = socket.assigns.casing_filter new_value = if current == casing, do: nil, else: casing socket = socket |> assign(:casing_filter, new_value) |> load_products() {:noreply, socket} end @impl true def handle_event("filter_grain_weight", %{"weight" => weight}, socket) do current = socket.assigns.grain_weight_filter weight_int = String.to_integer(weight) new_value = if current == weight_int, do: nil, else: weight_int socket = socket |> assign(:grain_weight_filter, new_value) |> load_products() {:noreply, socket} end @impl true def handle_event("toggle_subsonic_filter", _params, socket) do current = socket.assigns.subsonic_filter new_value = if current == true, do: nil, else: true socket = socket |> assign(:subsonic_filter, new_value) |> load_products() {:noreply, socket} end @impl true def handle_event("change_range", %{"range" => range}, socket) do socket = socket |> assign(:chart_range, range) |> load_chart_data() {:noreply, push_event(socket, "update-chart", %{data: socket.assigns.chart_data})} end @impl true def handle_info({:prices_updated, caliber_ids}, socket) do if socket.assigns.caliber.id in caliber_ids do socket = socket |> load_filter_options() |> load_products() |> load_chart_data() |> load_price_stats() {:noreply, push_event(socket, "update-chart", %{data: socket.assigns.chart_data})} else {:noreply, socket} end end defp load_filter_options(socket) do caliber_id = socket.assigns.caliber.id assign(socket, available_casings: Catalog.distinct_casings_for_caliber(caliber_id), available_grain_weights: Catalog.distinct_grain_weights_for_caliber(caliber_id), has_subsonic_products: Catalog.has_subsonic_products?(caliber_id) ) end defp load_products(socket) do %{ caliber: caliber, in_stock_filter: in_stock_filter, casing_filter: casing_filter, grain_weight_filter: grain_weight_filter, subsonic_filter: subsonic_filter } = socket.assigns opts = [limit: 200] opts = if in_stock_filter, do: Keyword.put(opts, :in_stock, true), else: opts opts = if casing_filter, do: Keyword.put(opts, :casing, casing_filter), else: opts opts = if grain_weight_filter, do: Keyword.put(opts, :grain_weight, grain_weight_filter), else: opts opts = if subsonic_filter, do: Keyword.put(opts, :subsonic, true), else: opts snapshots = Prices.latest_prices_for_caliber(caliber.id, opts) product_ids = Enum.map(snapshots, & &1.product_id) products_map = if product_ids == [] do %{} else import Ecto.Query from(p in Catalog.Product, where: p.id in ^product_ids, preload: [:retailer] ) |> Ammoprices.Repo.all() |> Map.new(&{&1.id, &1}) end rows = Enum.map(snapshots, fn snap -> product = Map.get(products_map, snap.product_id) %{ id: snap.id, product: product, price_cents: snap.price_cents, price_per_round_cents: snap.price_per_round_cents, in_stock: snap.in_stock } end) stream(socket, :products, rows, reset: true) end defp load_chart_data(socket) do %{caliber: caliber, chart_range: range} = socket.assigns days = Map.get(@range_days, range, 30) daily_data = Prices.daily_averages_for_caliber(caliber.id, days: days) {labels, avgs, mins} = Enum.reduce(daily_data, {[], [], []}, fn d, {ls, as, ms} -> {[Date.to_iso8601(d.date) | ls], [d.avg_ppr | as], [d.min_ppr | ms]} end) chart_data = %{ labels: Enum.reverse(labels), avg: Enum.reverse(avgs), min: Enum.reverse(mins) } assign(socket, chart_data: chart_data, chart_empty?: daily_data == []) end defp load_price_stats(socket) do stats = Prices.price_stats_for_caliber(socket.assigns.caliber.id) assign(socket, :price_stats, stats) end defp format_stat_cpr(nil), do: "--" defp format_stat_cpr(cents) do "$#{:erlang.float_to_binary(cents / 100, decimals: 2)}" end @impl true def render(assigns) do ~H"""
<%!-- Breadcrumb + Heading --%>

{@caliber.name}

<%!-- Price Stats Banner --%>
Current Low
{format_stat_cpr(@price_stats.current_min)}
30-Day Avg
{format_stat_cpr(@price_stats.thirty_day_avg)}
All-Time Low
{format_stat_cpr(@price_stats.all_time_low)}
All-Time High
{format_stat_cpr(@price_stats.all_time_high)}
<%!-- Price Chart --%>

Price History

No price history available for this range yet.

<%!-- Filters --%>
<%!-- In Stock toggle --%> <%!-- Subsonic toggle --%> <%!-- Casing filter buttons --%> <%= if @available_casings != [] do %> | <% end %> <%!-- Grain weight filter buttons --%> <%= if @available_grain_weights != [] do %> | <% end %>
<%!-- Product Table --%>
Product CPR
{row.product.retailer.name} {row.product.brand}
<.price_per_round cents={row.price_per_round_cents} class="text-sm font-bold text-success" />
<%!-- Colocated Chart.js Hook --%>
""" end end