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

<%!-- 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"""

{@label}

{@value || "—"}

""" 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"""
{@label}
{@value}
""" 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