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:
parent
f075ab55fb
commit
525692fe05
4 changed files with 72 additions and 21 deletions
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue