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 = %{
|
chart_data = %{
|
||||||
labels: Enum.map(daily_data, &Date.to_iso8601(&1.date)),
|
labels: Enum.map(daily_data, &Date.to_iso8601(&1.date)),
|
||||||
avg: Enum.map(daily_data, & &1.avg_ppr),
|
avg: Enum.map(daily_data, & &1.avg_ppr),
|
||||||
min: Enum.map(daily_data, & &1.min_ppr),
|
min: Enum.map(daily_data, & &1.min_ppr)
|
||||||
max: Enum.map(daily_data, & &1.max_ppr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assign(socket, :chart_data, chart_data)
|
assign(socket, chart_data: chart_data, chart_empty?: daily_data == [])
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_price_stats(socket) do
|
defp load_price_stats(socket) do
|
||||||
|
|
@ -279,14 +278,25 @@ defmodule AmmopricesWeb.CaliberLive.Show do
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="relative h-64 sm:h-80">
|
||||||
id="price-chart-container"
|
<div
|
||||||
phx-hook=".PriceChart"
|
id="price-chart-container"
|
||||||
phx-update="ignore"
|
phx-hook=".PriceChart"
|
||||||
data-chart-data={Jason.encode!(@chart_data)}
|
phx-update="ignore"
|
||||||
class="h-64 sm:h-80"
|
data-chart-data={Jason.encode!(@chart_data)}
|
||||||
>
|
class="w-full h-full"
|
||||||
<canvas id="price-chart" class="w-full h-full"></canvas>
|
>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -504,10 +514,6 @@ defmodule AmmopricesWeb.CaliberLive.Show do
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d")
|
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, {
|
return new Chart(ctx, {
|
||||||
type: "line",
|
type: "line",
|
||||||
data: {
|
data: {
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@ defmodule AmmopricesWeb.HomeLive do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
if connected?(socket) do
|
||||||
|
Phoenix.PubSub.subscribe(Ammoprices.PubSub, "prices:updated")
|
||||||
|
end
|
||||||
|
|
||||||
calibers = Catalog.list_calibers()
|
calibers = Catalog.list_calibers()
|
||||||
cheapest = Prices.cheapest_per_caliber()
|
|
||||||
cheapest_map = Map.new(cheapest, fn r -> {r.caliber_id, r.min_ppr} end)
|
|
||||||
|
|
||||||
grouped =
|
grouped =
|
||||||
calibers
|
calibers
|
||||||
|
|
@ -28,12 +30,25 @@ defmodule AmmopricesWeb.HomeLive do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
assign(socket,
|
socket
|
||||||
|
|> assign(
|
||||||
page_title: "AmmoCPR",
|
page_title: "AmmoCPR",
|
||||||
categories: @categories,
|
categories: @categories,
|
||||||
calibers_by_category: grouped,
|
calibers_by_category: grouped
|
||||||
cheapest_map: cheapest_map
|
)
|
||||||
)}
|
|> 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
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,22 @@ defmodule AmmopricesWeb.CaliberLive.ShowTest do
|
||||||
assert has_element?(view, "#filter-casing-steel")
|
assert has_element?(view, "#filter-casing-steel")
|
||||||
end
|
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", %{
|
test "chart container uses phx-update=ignore to survive DOM patches", %{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
caliber: caliber,
|
caliber: caliber,
|
||||||
|
|
|
||||||
|
|
@ -33,5 +33,19 @@ defmodule AmmopricesWeb.HomeLiveTest do
|
||||||
|
|
||||||
assert html =~ "25"
|
assert html =~ "25"
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue