Packages
phoenix_kit
1.7.69
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit/dashboard/badge.ex
defmodule PhoenixKit.Dashboard.Badge do
@moduledoc """
Defines badge types for dashboard tab indicators.
Badges provide visual feedback on tabs to show counts, status, or draw attention.
They can be static or update live via PubSub subscriptions.
## Badge Types
- `:count` - Numeric count badge (e.g., "5", "99+")
- `:dot` - Simple colored dot indicator
- `:status` - Status indicator with color (green/yellow/red)
- `:new` - "New" text indicator
- `:text` - Custom text badge
## Static Badge
%Badge{type: :count, value: 5}
%Badge{type: :dot, color: :success}
%Badge{type: :status, value: :online, color: :success}
## Live Badge with PubSub
%Badge{
type: :count,
subscribe: {"farm:stats", fn msg -> msg.printing_count end}
}
## Context-Aware Badge
For badges that show different values per user/organization/context, use `context_key`
and optionally `loader`. The badge value is stored per-context in socket assigns
instead of globally.
# Badge depends on current organization context
%Badge{
type: :count,
context_key: :organization,
loader: {MyApp.Alerts, :count_for_org}, # Called with (context)
subscribe: "org:{id}:alerts" # {id} replaced with context.id
}
### Context Placeholders
Subscribe topics support `{field}` placeholders that are resolved from the current context:
- `{id}` - context.id or context[:id]
- `{name}` - context.name or context[:name]
- Any field accessible on the context struct/map
### Loader Function
The loader is called when the LiveView mounts to get the initial badge value:
- `{Module, :function}` - Called as `Module.function(context)`
- `fn context -> value end` - Anonymous function
## Badge with Attention Animation
%Badge{
type: :count,
value: 3,
color: :error,
pulse: true
}
"""
@type badge_type :: :count | :dot | :status | :new | :text | :compound
@type badge_color ::
:primary | :secondary | :accent | :info | :success | :warning | :error | :neutral
@type subscribe_config :: {String.t(), (map() -> any())} | {String.t(), atom()} | String.t()
@type loader_config :: {module(), atom()} | (any() -> any())
@type compound_segment :: %{
required(:value) => integer() | String.t(),
required(:color) => badge_color(),
optional(:label) => String.t()
}
@type compound_style :: :text | :blocks | :dots
@type t :: %__MODULE__{
type: badge_type(),
value: any(),
color: badge_color(),
max: integer() | nil,
pulse: boolean(),
animate: boolean(),
hidden_when_zero: boolean(),
subscribe: subscribe_config() | nil,
format: (any() -> String.t()) | nil,
metadata: map(),
context_key: atom() | nil,
loader: loader_config() | nil,
# Compound badge fields
segments: list(compound_segment()),
compound_style: compound_style(),
separator: String.t(),
hide_zero_segments: boolean()
}
defstruct [
:value,
:subscribe,
:format,
:max,
:context_key,
:loader,
type: :count,
color: :primary,
pulse: false,
animate: true,
hidden_when_zero: true,
metadata: %{},
# Compound badge fields
segments: [],
compound_style: :text,
separator: "/",
hide_zero_segments: false
]
@valid_types [:count, :dot, :status, :new, :text, :compound]
@valid_colors [:primary, :secondary, :accent, :info, :success, :warning, :error, :neutral]
@doc """
Creates a new Badge struct from a map or keyword list.
## Options
- `:type` - Badge type: :count, :dot, :status, :new, :text (default: :count)
- `:value` - The value to display (number for count, atom/string for status/text)
- `:color` - Badge color: :primary, :secondary, :accent, :info, :success, :warning, :error, :neutral (default: :primary)
- `:max` - Maximum display value for count badges (e.g., 99 shows "99+") (optional)
- `:pulse` - Enable pulse animation (default: false)
- `:animate` - Enable value change animation (default: true)
- `:hidden_when_zero` - Hide count badge when value is 0 (default: true)
- `:subscribe` - PubSub subscription config for live updates (optional)
- `:format` - Custom formatter function for the value (optional)
- `:metadata` - Custom metadata map (default: %{})
- `:context_key` - Context selector key for per-context badges (optional, e.g., :organization)
- `:loader` - Function to load initial value for context: `{Module, :function}` or `fn context -> value end`
## Subscribe Configuration
The `:subscribe` option can be:
1. A tuple of {topic, extractor_function}:
`{"farm:stats", fn msg -> msg.printing_count end}`
2. A tuple of {topic, key_atom} to extract from message:
`{"farm:stats", :printing_count}`
3. Just a topic string (uses full message as value):
`"user:notifications:count"`
Topics support `{field}` placeholders for context-aware badges:
`"org:{id}:alerts"` - resolves to `"org:123:alerts"` when context.id is 123
## Examples
iex> Badge.new(type: :count, value: 5)
{:ok, %Badge{type: :count, value: 5}}
iex> Badge.new(type: :dot, color: :error, pulse: true)
{:ok, %Badge{type: :dot, color: :error, pulse: true}}
iex> Badge.new(type: :count, subscribe: {"orders:count", :count})
{:ok, %Badge{type: :count, subscribe: {"orders:count", :count}}}
# Context-aware badge
iex> Badge.new(type: :count, context_key: :organization, loader: {MyApp.Alerts, :count_for_org})
{:ok, %Badge{type: :count, context_key: :organization, loader: {MyApp.Alerts, :count_for_org}}}
"""
@spec new(map() | keyword()) :: {:ok, t()} | {:error, String.t()}
def new(attrs) when is_list(attrs), do: new(Map.new(attrs))
def new(attrs) when is_map(attrs) do
type = parse_type(get_attr(attrs, :type))
color = parse_color(get_attr(attrs, :color))
cond do
type not in @valid_types ->
{:error, "Invalid badge type: #{inspect(type)}. Must be one of #{inspect(@valid_types)}"}
color not in @valid_colors ->
{:error,
"Invalid badge color: #{inspect(color)}. Must be one of #{inspect(@valid_colors)}"}
true ->
{:ok, build_badge_struct(attrs, type, color)}
end
end
defp build_badge_struct(attrs, type, color) do
%__MODULE__{
type: type,
value: get_attr(attrs, :value),
color: color,
max: get_attr(attrs, :max),
pulse: get_attr(attrs, :pulse) || false,
animate: get_attr_with_default(attrs, :animate, true),
hidden_when_zero: get_attr_with_default(attrs, :hidden_when_zero, true),
subscribe: parse_subscribe(get_attr(attrs, :subscribe)),
format: get_attr(attrs, :format),
metadata: get_attr(attrs, :metadata) || %{},
context_key: get_attr(attrs, :context_key),
loader: parse_loader(get_attr(attrs, :loader)),
# Compound badge fields
segments: parse_segments(get_attr(attrs, :segments)),
compound_style: parse_compound_style(get_attr(attrs, :compound_style)),
separator: get_attr(attrs, :separator) || "/",
hide_zero_segments: get_attr(attrs, :hide_zero_segments) || false
}
end
defp get_attr(attrs, key), do: attrs[key] || attrs[Atom.to_string(key)]
defp get_attr_with_default(attrs, key, default) do
Map.get(attrs, key, Map.get(attrs, Atom.to_string(key), default))
end
@doc """
Creates a new Badge struct, raising on error.
"""
@spec new!(map() | keyword()) :: t()
def new!(attrs) do
case new(attrs) do
{:ok, badge} -> badge
{:error, reason} -> raise ArgumentError, reason
end
end
# Convenience constructors
@doc """
Creates a count badge.
## Examples
Badge.count(5)
Badge.count(10, color: :error, max: 99)
"""
@spec count(integer(), keyword()) :: t()
def count(value, opts \\ []) do
new!(Keyword.merge([type: :count, value: value], opts))
end
@doc """
Creates a dot badge.
## Examples
Badge.dot()
Badge.dot(color: :success)
Badge.dot(color: :error, pulse: true)
"""
@spec dot(keyword()) :: t()
def dot(opts \\ []) do
new!(Keyword.merge([type: :dot], opts))
end
@doc """
Creates a status badge.
## Examples
Badge.status(:online, color: :success)
Badge.status(:busy, color: :warning)
Badge.status(:offline, color: :error)
"""
@spec status(atom() | String.t(), keyword()) :: t()
def status(value, opts \\ []) do
new!(Keyword.merge([type: :status, value: value], opts))
end
@doc """
Creates a "New" badge.
## Examples
Badge.new_indicator()
Badge.new_indicator(color: :accent)
"""
@spec new_indicator(keyword()) :: t()
def new_indicator(opts \\ []) do
new!(Keyword.merge([type: :new, value: "New"], opts))
end
@doc """
Creates a text badge with custom text.
## Examples
Badge.text("Beta")
Badge.text("Pro", color: :accent)
"""
@spec text(String.t(), keyword()) :: t()
def text(value, opts \\ []) do
new!(Keyword.merge([type: :text, value: value], opts))
end
@doc """
Creates a compound badge with multiple colored segments.
A compound badge displays multiple values with different colors in a single badge.
Useful for showing status breakdowns like "10 success / 5 pending / 2 error".
## Styles
- `:text` (default) - Colored text values with separator (e.g., "10 / 5 / 2")
- `:blocks` - Colored background pills side by side
- `:dots` - Colored dots with numbers beside them
## Options
- `:style` - Display style: `:text`, `:blocks`, `:dots` (default: `:text`)
- `:separator` - Separator string for `:text` style (default: "/")
- `:hide_zero_segments` - Hide segments with value 0 (default: false)
- `:pulse` - Enable pulse animation (default: false)
## Segment Format
Each segment is a map with:
- `:value` (required) - Integer or string value to display
- `:color` (required) - Badge color atom (:success, :warning, :error, etc.)
- `:label` (optional) - Text label shown after value (e.g., "done", "pending")
## Examples
# Simple compound badge
Badge.compound([
%{value: 10, color: :success},
%{value: 5, color: :warning},
%{value: 2, color: :error}
])
# With labels and blocks style
Badge.compound([
%{value: 10, color: :success, label: "done"},
%{value: 5, color: :warning, label: "pending"}
], style: :blocks)
# Hide zero segments
Badge.compound([
%{value: 10, color: :success},
%{value: 0, color: :error}
], hide_zero_segments: true)
# Only shows: "10"
"""
@spec compound(list(compound_segment()), keyword()) :: t()
def compound(segments, opts \\ []) when is_list(segments) do
new!(
Keyword.merge(
[
type: :compound,
segments: segments,
compound_style: Keyword.get(opts, :style, :text),
separator: Keyword.get(opts, :separator, "/"),
hide_zero_segments: Keyword.get(opts, :hide_zero_segments, false)
],
opts
)
)
end
@doc """
Creates a context-aware compound badge that loads segments per-context.
The loader function should return a list of segment maps.
## Examples
# Loader returns list of segments for current organization
Badge.compound_context(:organization, {MyApp.Tasks, :get_status_counts},
style: :blocks
)
# In MyApp.Tasks
def get_status_counts(org) do
[
%{value: count_completed(org.id), color: :success},
%{value: count_pending(org.id), color: :warning},
%{value: count_overdue(org.id), color: :error}
]
end
"""
@spec compound_context(atom(), loader_config(), keyword()) :: t()
def compound_context(context_key, loader, opts \\ []) do
new!(
Keyword.merge(
[
type: :compound,
context_key: context_key,
loader: loader,
compound_style: Keyword.get(opts, :style, :text),
separator: Keyword.get(opts, :separator, "/"),
hide_zero_segments: Keyword.get(opts, :hide_zero_segments, false)
],
opts
)
)
end
@doc """
Returns visible segments for a compound badge.
If `hide_zero_segments` is true, filters out segments with value 0 or nil.
## Examples
badge = Badge.compound([
%{value: 10, color: :success},
%{value: 0, color: :error}
], hide_zero_segments: true)
Badge.visible_segments(badge)
# => [%{value: 10, color: :success}]
"""
@spec visible_segments(t()) :: list(compound_segment())
def visible_segments(%__MODULE__{type: :compound, segments: segments, hide_zero_segments: true}) do
Enum.reject(segments, fn seg ->
value = seg[:value] || seg["value"]
value == 0 or is_nil(value)
end)
end
def visible_segments(%__MODULE__{type: :compound, segments: segments}), do: segments
def visible_segments(_), do: []
@doc """
Updates segments for a compound badge.
"""
@spec update_segments(t(), list(compound_segment())) :: t()
def update_segments(%__MODULE__{type: :compound} = badge, segments) when is_list(segments) do
%{badge | segments: segments}
end
def update_segments(badge, _segments), do: badge
@doc """
Creates a live badge that subscribes to PubSub updates.
## Examples
Badge.live("user:notifications", :count)
Badge.live("farm:stats", fn msg -> msg.printing_count end, color: :info)
"""
@spec live(String.t(), atom() | (map() -> any()), keyword()) :: t()
def live(topic, extractor, opts \\ []) do
subscribe =
case extractor do
key when is_atom(key) -> {topic, key}
fun when is_function(fun, 1) -> {topic, fun}
end
new!(Keyword.merge([type: :count, subscribe: subscribe], opts))
end
@doc """
Updates the badge value.
"""
@spec update_value(t(), any()) :: t()
def update_value(%__MODULE__{} = badge, value) do
%{badge | value: value}
end
@doc """
Formats the badge value for display.
## Examples
iex> badge = Badge.count(5)
iex> Badge.display_value(badge)
"5"
iex> badge = Badge.count(150, max: 99)
iex> Badge.display_value(badge)
"99+"
"""
@spec display_value(t()) :: String.t() | nil
def display_value(%__MODULE__{type: :dot}), do: nil
def display_value(%__MODULE__{type: :new}), do: "New"
def display_value(%__MODULE__{type: :count, value: value, max: max, format: format}) do
formatted =
cond do
is_function(format, 1) -> format.(value)
is_nil(value) -> nil
is_nil(max) -> to_string(value)
value > max -> "#{max}+"
true -> to_string(value)
end
formatted
end
def display_value(%__MODULE__{value: value, format: format}) when is_function(format, 1) do
format.(value)
end
def display_value(%__MODULE__{value: value}) when is_binary(value), do: value
def display_value(%__MODULE__{value: value}) when is_atom(value), do: Atom.to_string(value)
def display_value(%__MODULE__{value: value}), do: to_string(value)
@doc """
Checks if the badge should be visible.
Count badges with value 0 are hidden by default when hidden_when_zero is true.
Compound badges are hidden when all segments have zero value (and hide_zero_segments is true).
"""
@spec visible?(t()) :: boolean()
def visible?(%__MODULE__{type: :count, value: 0, hidden_when_zero: true}), do: false
def visible?(%__MODULE__{type: :count, value: nil, hidden_when_zero: true}), do: false
def visible?(%__MODULE__{type: :compound, segments: segments, hide_zero_segments: true}) do
Enum.any?(segments, fn seg ->
value = seg[:value] || seg["value"]
value != nil and value != 0
end)
end
def visible?(%__MODULE__{type: :compound, segments: []}), do: false
def visible?(%__MODULE__{}), do: true
@doc """
Extracts value from a PubSub message using the badge's subscription config.
"""
@spec extract_value(t(), map()) :: any()
def extract_value(%__MODULE__{subscribe: {_topic, extractor}}, message)
when is_function(extractor, 1) do
extractor.(message)
end
def extract_value(%__MODULE__{subscribe: {_topic, key}}, message) when is_atom(key) do
Map.get(message, key)
end
def extract_value(%__MODULE__{subscribe: _topic}, message) when is_binary(message) do
message
end
def extract_value(%__MODULE__{subscribe: _topic}, message) when is_map(message) do
message
end
def extract_value(_, message), do: message
@doc """
Gets the PubSub topic for this badge, if it has a subscription.
"""
@spec get_topic(t()) :: String.t() | nil
def get_topic(%__MODULE__{subscribe: {topic, _}}), do: topic
def get_topic(%__MODULE__{subscribe: topic}) when is_binary(topic), do: topic
def get_topic(_), do: nil
@doc """
Checks if this badge has a live subscription.
"""
@spec live?(t()) :: boolean()
def live?(%__MODULE__{subscribe: nil}), do: false
def live?(%__MODULE__{subscribe: _}), do: true
@doc """
Returns the CSS color class for the badge.
"""
@spec color_class(t()) :: String.t()
def color_class(%__MODULE__{color: :primary}), do: "badge-primary"
def color_class(%__MODULE__{color: :secondary}), do: "badge-secondary"
def color_class(%__MODULE__{color: :accent}), do: "badge-accent"
def color_class(%__MODULE__{color: :info}), do: "badge-info"
def color_class(%__MODULE__{color: :success}), do: "badge-success"
def color_class(%__MODULE__{color: :warning}), do: "badge-warning"
def color_class(%__MODULE__{color: :error}), do: "badge-error"
def color_class(%__MODULE__{color: :neutral}), do: "badge-neutral"
def color_class(_), do: "badge-primary"
@doc """
Returns the CSS background color class for dot badges.
"""
@spec dot_color_class(t()) :: String.t()
def dot_color_class(%__MODULE__{color: :primary}), do: "bg-primary"
def dot_color_class(%__MODULE__{color: :secondary}), do: "bg-secondary"
def dot_color_class(%__MODULE__{color: :accent}), do: "bg-accent"
def dot_color_class(%__MODULE__{color: :info}), do: "bg-info"
def dot_color_class(%__MODULE__{color: :success}), do: "bg-success"
def dot_color_class(%__MODULE__{color: :warning}), do: "bg-warning"
def dot_color_class(%__MODULE__{color: :error}), do: "bg-error"
def dot_color_class(%__MODULE__{color: :neutral}), do: "bg-neutral"
def dot_color_class(_), do: "bg-primary"
# Private helpers
defp parse_type(:count), do: :count
defp parse_type(:dot), do: :dot
defp parse_type(:status), do: :status
defp parse_type(:new), do: :new
defp parse_type(:text), do: :text
defp parse_type(:compound), do: :compound
defp parse_type("count"), do: :count
defp parse_type("dot"), do: :dot
defp parse_type("status"), do: :status
defp parse_type("new"), do: :new
defp parse_type("text"), do: :text
defp parse_type("compound"), do: :compound
defp parse_type(nil), do: :count
defp parse_type(other), do: other
defp parse_color(:primary), do: :primary
defp parse_color(:secondary), do: :secondary
defp parse_color(:accent), do: :accent
defp parse_color(:info), do: :info
defp parse_color(:success), do: :success
defp parse_color(:warning), do: :warning
defp parse_color(:error), do: :error
defp parse_color(:neutral), do: :neutral
defp parse_color("primary"), do: :primary
defp parse_color("secondary"), do: :secondary
defp parse_color("accent"), do: :accent
defp parse_color("info"), do: :info
defp parse_color("success"), do: :success
defp parse_color("warning"), do: :warning
defp parse_color("error"), do: :error
defp parse_color("neutral"), do: :neutral
defp parse_color(nil), do: :primary
defp parse_color(other), do: other
defp parse_subscribe(nil), do: nil
defp parse_subscribe({topic, extractor}) when is_binary(topic), do: {topic, extractor}
defp parse_subscribe(topic) when is_binary(topic), do: topic
defp parse_subscribe(_), do: nil
defp parse_loader(nil), do: nil
defp parse_loader({mod, fun}) when is_atom(mod) and is_atom(fun), do: {mod, fun}
defp parse_loader(fun) when is_function(fun, 1), do: fun
defp parse_loader(_), do: nil
defp parse_segments(nil), do: []
defp parse_segments(segments) when is_list(segments), do: segments
defp parse_segments(_), do: []
defp parse_compound_style(:text), do: :text
defp parse_compound_style(:blocks), do: :blocks
defp parse_compound_style(:dots), do: :dots
defp parse_compound_style("text"), do: :text
defp parse_compound_style("blocks"), do: :blocks
defp parse_compound_style("dots"), do: :dots
defp parse_compound_style(nil), do: :text
defp parse_compound_style(_), do: :text
# Context-aware badge functions
@doc """
Creates a context-aware badge that loads values per-context.
## Examples
# Badge that shows alert count for current organization
Badge.context(:organization, {MyApp.Alerts, :count_for_org}, color: :error)
# With live updates via context-specific PubSub topic
Badge.context(:farm, {MyApp.Farms, :printing_count},
subscribe: "farm:{id}:stats",
color: :info
)
"""
@spec context(atom(), loader_config(), keyword()) :: t()
def context(context_key, loader, opts \\ []) do
new!(Keyword.merge([type: :count, context_key: context_key, loader: loader], opts))
end
@doc """
Checks if this badge is context-aware (requires per-context value storage).
"""
@spec context_aware?(t()) :: boolean()
def context_aware?(%__MODULE__{context_key: nil}), do: false
def context_aware?(%__MODULE__{context_key: _}), do: true
@doc """
Resolves placeholders in a topic string using context data.
Supports `{field}` syntax where field is accessed from the context.
## Examples
iex> Badge.resolve_topic("org:{id}:alerts", %{id: 123})
"org:123:alerts"
iex> Badge.resolve_topic("farm:{farm_id}:stats", %{farm_id: "abc"})
"farm:abc:stats"
"""
@spec resolve_topic(String.t() | nil, map() | struct()) :: String.t() | nil
def resolve_topic(nil, _context), do: nil
def resolve_topic(topic, context) when is_binary(topic) do
Regex.replace(~r/\{(\w+)\}/, topic, fn _match, field ->
field_atom = String.to_existing_atom(field)
value = get_context_field(context, field_atom)
to_string(value)
end)
rescue
ArgumentError -> topic
end
@doc """
Gets the resolved topic for this badge given a context.
For context-aware badges, resolves placeholders. For regular badges, returns the topic as-is.
"""
@spec get_resolved_topic(t(), map() | struct() | nil) :: String.t() | nil
def get_resolved_topic(%__MODULE__{} = badge, context) do
topic = get_topic(badge)
if context_aware?(badge) and topic do
resolve_topic(topic, context)
else
topic
end
end
@doc """
Loads the initial badge value using the loader function.
## Examples
iex> badge = Badge.context(:org, {MyApp.Alerts, :count_for_org})
iex> Badge.load_value(badge, %{id: 123})
5 # Result from MyApp.Alerts.count_for_org(%{id: 123})
"""
@spec load_value(t(), map() | struct() | nil) :: any()
def load_value(%__MODULE__{loader: nil}, _context), do: nil
def load_value(%__MODULE__{loader: {mod, fun}}, context) do
apply(mod, fun, [context])
rescue
_ -> nil
end
def load_value(%__MODULE__{loader: fun}, context) when is_function(fun, 1) do
fun.(context)
rescue
_ -> nil
end
def load_value(_, _), do: nil
# Helper to get a field from context (supports both maps and structs)
defp get_context_field(context, field) when is_map(context) do
Map.get(context, field) || Map.get(context, Atom.to_string(field))
end
defp get_context_field(context, field) when is_struct(context) do
Map.get(context, field)
end
defp get_context_field(_, _), do: nil
end