Add chart empty state and home-page real-time price refresh

- Show "No price history available for this range yet." overlay when
  the caliber chart range contains no daily averages (previously the
  chart rendered as an unlabeled empty grid).
- Subscribe HomeLive to the "prices:updated" PubSub topic so the
  cheapest-per-caliber grid refreshes without a full page reload.
- Drop the unused textColor computation and unused max series from the
  chart hook/data.
This commit is contained in:
Graham McIntire 2026-04-22 15:28:02 -05:00
parent f075ab55fb
commit 525692fe05
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
4 changed files with 72 additions and 21 deletions

View file

@ -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
</button>
</div>
</div>
<div
id="price-chart-container"
phx-hook=".PriceChart"
phx-update="ignore"
data-chart-data={Jason.encode!(@chart_data)}
class="h-64 sm:h-80"
>
<canvas id="price-chart" class="w-full h-full"></canvas>
<div class="relative h-64 sm:h-80">
<div
id="price-chart-container"
phx-hook=".PriceChart"
phx-update="ignore"
data-chart-data={Jason.encode!(@chart_data)}
class="w-full h-full"
>
<canvas id="price-chart" class="w-full h-full"></canvas>
</div>
<div
:if={@chart_empty?}
id="price-chart-empty"
class="absolute inset-0 flex items-center justify-center pointer-events-none"
>
<p class="text-sm text-base-content/40">
No price history available for this range yet.
</p>
</div>
</div>
</div>
@ -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: {

View file

@ -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

View file

@ -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,

View file

@ -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