ammocpr/lib/ammoprices_web/components/price_components.ex
Graham McIntire bbe5bde145
Initial implementation of ammo price tracker
Phoenix 1.8 app that scrapes ammunition retailers (Lucky Gunner,
SGAmmo) for price data and displays historical price trends.

- Data model: retailers, calibers, products, price snapshots
- Scraper infrastructure with Req, Floki, realistic browser headers
- Oban-scheduled scrape jobs (every 4h with randomized delays)
- LiveView pages: homepage with category cards, caliber detail with
  price table, Chart.js price history, and price stats banner
- 18 seeded calibers across handgun/rifle/rimfire/shotgun categories
- 77 tests
2026-03-11 15:58:12 -05:00

79 lines
1.9 KiB
Elixir

defmodule AmmopricesWeb.PriceComponents do
@moduledoc false
use Phoenix.Component
attr :cents, :integer, required: true
attr :class, :string, default: nil
def price_per_round(assigns) do
~H"""
<span class={["font-mono tabular-nums", @class]}>
{format_cpr(@cents)}
</span>
"""
end
attr :in_stock, :boolean, required: true
attr :class, :string, default: nil
def stock_badge(assigns) do
~H"""
<span class={[
"inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wider",
if(@in_stock, do: "bg-success/15 text-success", else: "bg-error/15 text-error"),
@class
]}>
<span class={[
"inline-block w-1.5 h-1.5 rounded-full",
if(@in_stock, do: "bg-success", else: "bg-error")
]} />
{if @in_stock, do: "In Stock", else: "Out of Stock"}
</span>
"""
end
attr :url, :string, required: true
attr :name, :string, required: true
attr :class, :string, default: nil
def retailer_link(assigns) do
~H"""
<a
href={@url}
target="_blank"
rel="noopener noreferrer"
class={[
"inline-flex items-center gap-1 text-primary hover:text-primary/80 transition-colors",
@class
]}
>
{@name}
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-3 h-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
"""
end
defp format_cpr(nil), do: "--"
defp format_cpr(cents) when cents < 100 do
"#{cents}¢/rd"
end
defp format_cpr(cents) do
dollars = cents / 100
"$#{:erlang.float_to_binary(dollars, decimals: 2)}/rd"
end
end