Current section

Files

Jump to
phoenix_kit_ecommerce lib phoenix_kit_ecommerce web helpers.ex
Raw

lib/phoenix_kit_ecommerce/web/helpers.ex

defmodule PhoenixKitEcommerce.Web.Helpers do
@moduledoc """
Shared helper functions for Shop public LiveViews.
Centralizes utility functions that were duplicated across shop_catalog,
catalog_category, catalog_product, cart_page, checkout_page, and checkout_complete.
"""
alias PhoenixKit.Modules.Languages
alias PhoenixKit.Modules.Languages.DialectMapper
alias PhoenixKit.Modules.Storage
alias PhoenixKit.Modules.Storage.URLSigner
alias PhoenixKit.Utils.Routes
alias PhoenixKitBilling.Currency
alias PhoenixKitEcommerce.Translations
# ---------------------------------------------------------------------------
# Price formatting
# ---------------------------------------------------------------------------
@doc "Format a price value with currency. Returns \"-\" for nil price."
def format_price(nil, _currency), do: "-"
def format_price(price, nil) do
"$#{Decimal.round(price, 2)}"
end
def format_price(price, currency) do
Currency.format_amount(price, currency)
end
# ---------------------------------------------------------------------------
# Current user
# ---------------------------------------------------------------------------
@doc "Extract current user from socket assigns scope."
def get_current_user(socket) do
case socket.assigns[:phoenix_kit_current_scope] do
%{user: %{uuid: _} = user} -> user
_ -> nil
end
end
# ---------------------------------------------------------------------------
# Language helpers
# ---------------------------------------------------------------------------
@doc """
Determine language from URL params.
Uses locale param if present, otherwise falls back to Translations.default_language/0.
Used by catalog and category pages (non-product pages).
"""
def get_language_from_params_or_default(%{"locale" => locale}) when is_binary(locale) do
DialectMapper.resolve_dialect(locale)
end
def get_language_from_params_or_default(_params) do
Translations.default_language()
end
@doc """
Find the best enabled language that has a slug for this entity.
Prefers the default language, then checks other enabled languages.
Returns nil if no valid language found.
"""
def best_redirect_language(slug_map) when slug_map == %{}, do: nil
def best_redirect_language(slug_map) do
enabled = Languages.get_enabled_languages()
default_first = Enum.sort_by(enabled, fn l -> if l.is_default, do: 0, else: 1 end)
Enum.find_value(default_first, fn lang ->
code = lang.code
base = DialectMapper.extract_base(code)
if Map.has_key?(slug_map, code) or Map.has_key?(slug_map, base), do: code
end)
end
@doc """
Build a localized URL path, adding language prefix for non-default languages.
Delegates to Routes.path which handles default vs non-default consistently.
"""
def build_lang_url(path, lang) do
base = DialectMapper.extract_base(lang)
Routes.path(path, locale: base)
end
# ---------------------------------------------------------------------------
# Pagination helpers
# ---------------------------------------------------------------------------
@doc "Parse page param with validation. Returns 1 for invalid/missing values."
def parse_page(nil), do: 1
def parse_page(""), do: 1
def parse_page(page) when is_binary(page) do
case Integer.parse(page) do
{p, ""} when p > 0 -> p
_ -> 1
end
end
def parse_page(page) when is_integer(page) and page > 0, do: page
def parse_page(_), do: 1
# ---------------------------------------------------------------------------
# Image helpers (for catalog list pages - uses featured_image_uuid)
# ---------------------------------------------------------------------------
@doc """
Get the first image URL for a product.
Handles Storage-based images (new format with featured_image_uuid or image_uuids)
and legacy URL-based images (Shopify imports).
Returns nil if no image is available.
"""
def first_image(%{featured_image_uuid: id}) when is_binary(id) do
get_storage_image_url(id, "small")
end
def first_image(%{image_uuids: [id | _]}) when is_binary(id) do
get_storage_image_url(id, "small")
end
# Legacy URL-based images (Shopify imports)
def first_image(%{images: [%{"src" => src} | _]}), do: src
def first_image(%{images: [first | _]}) when is_binary(first), do: first
def first_image(_), do: nil
@doc """
Get signed URL for a Storage image file.
Returns nil if file or variant not found (unlike product detail page
which returns a placeholder). Falls back to original variant if
requested variant is not available.
"""
def get_storage_image_url(file_uuid, variant) do
case Storage.get_file(file_uuid) do
%{uuid: uuid} ->
resolve_storage_variant(file_uuid, uuid, variant)
nil ->
nil
end
end
defp resolve_storage_variant(file_uuid, uuid, variant) do
case Storage.get_file_instance_by_name(uuid, variant) do
nil ->
case Storage.get_file_instance_by_name(uuid, "original") do
nil -> nil
_instance -> URLSigner.signed_url(file_uuid, "original")
end
_instance ->
URLSigner.signed_url(file_uuid, variant)
end
end
# ---------------------------------------------------------------------------
# UI helpers
# ---------------------------------------------------------------------------
@doc """
Convert a key string to human-readable format.
Example: "material_type" -> "Material Type"
"""
def humanize_key(key) when is_binary(key) do
key
|> String.replace("_", " ")
|> String.split(" ")
|> Enum.map_join(" ", &String.capitalize/1)
end
def humanize_key(key), do: to_string(key)
# ---------------------------------------------------------------------------
# Billing profile helpers
# ---------------------------------------------------------------------------
@doc "Format display name for a billing profile."
def profile_display_name(%{type: "company"} = profile) do
profile.company_name || "#{profile.first_name} #{profile.last_name}"
end
def profile_display_name(profile) do
"#{profile.first_name} #{profile.last_name}"
end
@doc "Format address for a billing profile."
def profile_address(profile) do
[profile.address_line1, profile.city, profile.postal_code, profile.country]
|> Enum.filter(& &1)
|> Enum.join(", ")
end
end