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
79 lines
1.9 KiB
Elixir
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
|