diff --git a/lib/ammoprices_web/live/caliber_live/show.ex b/lib/ammoprices_web/live/caliber_live/show.ex
index f35e2f7..b7b75ca 100644
--- a/lib/ammoprices_web/live/caliber_live/show.ex
+++ b/lib/ammoprices_web/live/caliber_live/show.ex
@@ -181,11 +181,10 @@ defmodule AmmopricesWeb.CaliberLive.Show do
chart_data = %{
labels: Enum.map(daily_data, &Date.to_iso8601(&1.date)),
avg: Enum.map(daily_data, & &1.avg_ppr),
- min: Enum.map(daily_data, & &1.min_ppr),
- max: Enum.map(daily_data, & &1.max_ppr)
+ min: Enum.map(daily_data, & &1.min_ppr)
}
- assign(socket, :chart_data, chart_data)
+ assign(socket, chart_data: chart_data, chart_empty?: daily_data == [])
end
defp load_price_stats(socket) do
@@ -279,14 +278,25 @@ defmodule AmmopricesWeb.CaliberLive.Show do
-
-
+
+
+
+
+
+
+ No price history available for this range yet.
+
+
@@ -504,10 +514,6 @@ defmodule AmmopricesWeb.CaliberLive.Show do
const ctx = canvas.getContext("2d")
- // Get CSS custom property values for theming
- const style = getComputedStyle(document.documentElement)
- const textColor = style.getPropertyValue("color") || "#666"
-
return new Chart(ctx, {
type: "line",
data: {
diff --git a/lib/ammoprices_web/live/home_live.ex b/lib/ammoprices_web/live/home_live.ex
index d04a6bf..4ff1533 100644
--- a/lib/ammoprices_web/live/home_live.ex
+++ b/lib/ammoprices_web/live/home_live.ex
@@ -16,9 +16,11 @@ defmodule AmmopricesWeb.HomeLive do
@impl true
def mount(_params, _session, socket) do
+ if connected?(socket) do
+ Phoenix.PubSub.subscribe(Ammoprices.PubSub, "prices:updated")
+ end
+
calibers = Catalog.list_calibers()
- cheapest = Prices.cheapest_per_caliber()
- cheapest_map = Map.new(cheapest, fn r -> {r.caliber_id, r.min_ppr} end)
grouped =
calibers
@@ -28,12 +30,25 @@ defmodule AmmopricesWeb.HomeLive do
end)
{:ok,
- assign(socket,
+ socket
+ |> assign(
page_title: "AmmoCPR",
categories: @categories,
- calibers_by_category: grouped,
- cheapest_map: cheapest_map
- )}
+ calibers_by_category: grouped
+ )
+ |> assign_cheapest_map()}
+ end
+
+ @impl true
+ def handle_info({:prices_updated, _}, socket) do
+ {:noreply, assign_cheapest_map(socket)}
+ end
+
+ defp assign_cheapest_map(socket) do
+ cheapest_map =
+ Map.new(Prices.cheapest_per_caliber(), fn r -> {r.caliber_id, r.min_ppr} end)
+
+ assign(socket, :cheapest_map, cheapest_map)
end
@impl true
diff --git a/test/ammoprices_web/live/caliber_live/show_test.exs b/test/ammoprices_web/live/caliber_live/show_test.exs
index 5fc4840..c26e00c 100644
--- a/test/ammoprices_web/live/caliber_live/show_test.exs
+++ b/test/ammoprices_web/live/caliber_live/show_test.exs
@@ -182,6 +182,22 @@ defmodule AmmopricesWeb.CaliberLive.ShowTest do
assert has_element?(view, "#filter-casing-steel")
end
+ test "shows empty chart state when no data exists in range", %{conn: conn, caliber: caliber} do
+ {:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
+
+ assert has_element?(view, "#price-chart-empty")
+ end
+
+ test "hides empty chart state when data exists in range", %{conn: conn, caliber: caliber, retailer: retailer} do
+ product = product_fixture(%{caliber: caliber, retailer: retailer, in_stock: true})
+ now = DateTime.truncate(DateTime.utc_now(), :second)
+ snapshot_fixture(%{product: product, price_per_round_cents: 28, in_stock: true, recorded_at: now})
+
+ {:ok, view, _html} = live(conn, "/calibers/#{caliber.slug}")
+
+ refute has_element?(view, "#price-chart-empty")
+ end
+
test "chart container uses phx-update=ignore to survive DOM patches", %{
conn: conn,
caliber: caliber,
diff --git a/test/ammoprices_web/live/home_live_test.exs b/test/ammoprices_web/live/home_live_test.exs
index 5771375..3c4001d 100644
--- a/test/ammoprices_web/live/home_live_test.exs
+++ b/test/ammoprices_web/live/home_live_test.exs
@@ -33,5 +33,19 @@ defmodule AmmopricesWeb.HomeLiveTest do
assert html =~ "25"
end
+
+ test "updates cheapest prices via PubSub broadcast", %{conn: conn} do
+ caliber = caliber_fixture(%{name: "9mm Luger", slug: "9mm-luger", category: "handgun"})
+ retailer = retailer_fixture()
+ product = product_fixture(%{caliber: caliber, retailer: retailer, in_stock: true})
+ now = DateTime.truncate(DateTime.utc_now(), :second)
+
+ {:ok, view, _html} = live(conn, "/")
+
+ snapshot_fixture(%{product: product, price_per_round_cents: 17, in_stock: true, recorded_at: now})
+ Phoenix.PubSub.broadcast(Ammoprices.PubSub, "prices:updated", {:prices_updated, %{}})
+
+ assert render(view) =~ "0.17"
+ end
end
end