defmodule CobblemonUiWeb.DashboardLive do
use CobblemonUiWeb, :live_view
@impl true
def mount(_params, _session, socket) do
players =
case CobblemonUi.CobblemonFS.list_players() do
{:ok, list} -> list
end
{:ok,
assign(socket,
page_title: "Cobblemon Dashboard",
players: players,
selected_player: nil,
player_data: nil,
selected_pokemon: nil,
view_mode: :party,
loading: false,
error: nil
)}
end
@impl true
def handle_params(%{"uuid" => uuid}, _uri, socket) do
case CobblemonUi.CobblemonFS.get_player(uuid) do
{:ok, data} ->
{:noreply,
assign(socket,
selected_player: uuid,
player_data: data,
selected_pokemon: nil,
error: nil
)}
{:error, :not_found} ->
{:noreply,
assign(socket,
selected_player: uuid,
player_data: nil,
error: "Player not found"
)}
{:error, reason} ->
{:noreply,
assign(socket,
selected_player: uuid,
player_data: nil,
error: "Error loading player: #{inspect(reason)}"
)}
end
end
def handle_params(_params, _uri, socket) do
{:noreply, assign(socket, selected_player: nil, player_data: nil, selected_pokemon: nil)}
end
@impl true
def handle_event("select_pokemon", %{"index" => index_str}, socket) do
index = String.to_integer(index_str)
pokemon =
case socket.assigns.view_mode do
:party ->
Enum.at(socket.assigns.player_data.party, index)
:pc ->
socket.assigns.player_data.pc
|> Enum.flat_map(fn box -> box.pokemon end)
|> Enum.at(index)
end
{:noreply, assign(socket, selected_pokemon: pokemon)}
end
def handle_event("close_pokemon", _params, socket) do
{:noreply, assign(socket, selected_pokemon: nil)}
end
def handle_event("switch_view", %{"mode" => mode}, socket) do
{:noreply, assign(socket, view_mode: String.to_existing_atom(mode), selected_pokemon: nil)}
end
def handle_event("refresh", _params, socket) do
players =
case CobblemonUi.CobblemonFS.list_players() do
{:ok, list} -> list
end
socket =
if uuid = socket.assigns.selected_player do
case CobblemonUi.CobblemonFS.get_player(uuid) do
{:ok, data} -> assign(socket, player_data: data, error: nil)
_ -> socket
end
else
socket
end
{:noreply, assign(socket, players: players)}
end
@impl true
def render(assigns) do
~H"""
<%!-- Header --%>
<.icon name="hero-cube-transparent" class="size-6 text-primary" />
Cobblemon
Player Data Explorer
<.link
navigate={~p"/battles"}
class="btn btn-ghost btn-sm gap-2 hover:bg-base-300/50 transition-colors"
>
<.icon name="hero-bolt" class="size-4 text-error" /> Battles
<%!-- Sidebar: Player List --%>
<%!-- Main Content --%>
<.icon name="hero-exclamation-triangle" class="size-5" />
{@error}
<%!-- Empty state --%>
<.icon
name="hero-arrow-left"
class="size-10 mx-auto mb-4 text-base-content/20"
/>
Select a player to explore
Choose from the sidebar to view their Pokémon
<%!-- Player data --%>
<%!-- Player header --%>
<.icon name="hero-user" class="size-5 text-primary/70" />
{player_name(@players, @player_data.uuid)}
{@player_data.uuid}
{party_count(@player_data)} in party
{pc_count(@player_data)} in PC
<%!-- View mode tabs --%>
<%!-- Party view --%>
<%= for {pokemon, idx} <- Enum.with_index(@player_data.party) do %>
<.pokemon_card pokemon={pokemon} index={idx} />
<% end %>
<%!-- PC view --%>
Box {box.box + 1}
<%= for {pokemon, idx} <- Enum.with_index(box.pokemon) do %>
<.pokemon_card
pokemon={pokemon}
index={pc_global_index(@player_data.pc, box.box, idx)}
compact
/>
<% end %>
PC storage is empty
<%!-- Pokémon detail panel --%>
<.pokemon_detail :if={@selected_pokemon} pokemon={@selected_pokemon} />
"""
end
# --- Components ---
attr :pokemon, :map, required: true
attr :index, :integer, required: true
attr :compact, :boolean, default: false
defp pokemon_card(%{pokemon: nil} = assigns) do
~H"""
Empty
"""
end
defp pokemon_card(assigns) do
~H"""
"""
end
attr :pokemon, :map, required: true
defp pokemon_detail(assigns) do
~H"""
<%!-- Detail header --%>
<.icon name="hero-bolt" class="size-4 text-primary/70" />
{@pokemon.species || "Unknown"}
★
Level {@pokemon.level || "?"} · {String.capitalize(@pokemon.form || "default")} form
<%!-- Info Column --%>
Details
<.stat_pill label="Nature" value={@pokemon.nature} />
<.stat_pill label="Ability" value={@pokemon.ability} />
<.stat_pill label="Gender" value={@pokemon.gender} />
<.stat_pill label="Friendship" value={@pokemon.friendship} />
<%!-- Moves --%>
Moves
{format_move(move)}
No moves
<%!-- Stats Column --%>
<%!-- IVs --%>
IVs
<.stat_bar
:for={{stat, val} <- stat_list(@pokemon.ivs)}
label={format_stat(stat)}
value={val}
max={31}
/>
<%!-- EVs --%>
EVs
({ev_total(@pokemon.evs)}/510)
<.stat_bar
:for={{stat, val} <- stat_list(@pokemon.evs)}
label={format_stat(stat)}
value={val}
max={252}
/>
"""
end
attr :label, :string, required: true
attr :value, :any, required: true
defp stat_pill(assigns) do
~H"""
"""
end
attr :label, :string, required: true
attr :value, :integer, required: true
attr :max, :integer, required: true
defp stat_bar(assigns) do
pct = if assigns.max > 0, do: min(assigns.value / assigns.max * 100, 100), else: 0
color =
cond do
pct >= 90 -> "bg-success"
pct >= 60 -> "bg-info"
pct >= 30 -> "bg-warning"
true -> "bg-error/70"
end
assigns = assign(assigns, pct: pct, color: color)
~H"""
"""
end
# --- Helpers ---
defp party_count(%{party: party}), do: Enum.count(party, &(not is_nil(&1)))
defp pc_count(%{pc: pc}),
do: Enum.sum(Enum.map(pc, fn b -> Enum.count(b.pokemon, &(not is_nil(&1))) end))
defp pc_global_index(boxes, current_box, slot_index) do
offset =
boxes
|> Enum.filter(fn b -> b.box < current_box end)
|> Enum.sum_by(fn b -> length(b.pokemon) end)
offset + slot_index
end
defp gender_symbol("male"), do: "♂"
defp gender_symbol("female"), do: "♀"
defp gender_symbol(_), do: "—"
defp gender_color("male"), do: "text-info"
defp gender_color("female"), do: "text-error"
defp gender_color(_), do: "text-base-content/40"
defp format_move(move) when is_binary(move), do: String.replace(move, "_", " ")
defp format_move(_), do: "—"
defp format_stat(:hp), do: "HP"
defp format_stat(:attack), do: "ATK"
defp format_stat(:defense), do: "DEF"
defp format_stat(:special_attack), do: "SPA"
defp format_stat(:special_defense), do: "SPD"
defp format_stat(:speed), do: "SPE"
defp format_stat(other), do: to_string(other)
defp stat_list(stats) when is_map(stats) do
[:hp, :attack, :defense, :special_attack, :special_defense, :speed]
|> Enum.map(fn key -> {key, Map.get(stats, key, 0)} end)
end
defp stat_list(_), do: []
defp ev_total(evs) when is_map(evs) do
evs |> Map.values() |> Enum.sum()
end
defp ev_total(_), do: 0
defp player_name(players, uuid) do
case Enum.find(players, fn p -> p.uuid == uuid end) do
%{name: name} when is_binary(name) -> name
_ -> "Unknown"
end
end
end