This commit is contained in:
@@ -3,6 +3,8 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
if connected?(socket), do: :timer.send_interval(1000, self(), :tick)
|
||||||
|
|
||||||
players =
|
players =
|
||||||
case CobblemonUi.CobblemonFS.list_players() do
|
case CobblemonUi.CobblemonFS.list_players() do
|
||||||
{:ok, list} -> list
|
{:ok, list} -> list
|
||||||
@@ -86,6 +88,15 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("refresh", _params, socket) do
|
def handle_event("refresh", _params, socket) do
|
||||||
|
{:noreply, do_refresh(socket)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:tick, socket) do
|
||||||
|
{:noreply, do_refresh(socket)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_refresh(socket) do
|
||||||
players =
|
players =
|
||||||
case CobblemonUi.CobblemonFS.list_players() do
|
case CobblemonUi.CobblemonFS.list_players() do
|
||||||
{:ok, list} -> list
|
{:ok, list} -> list
|
||||||
@@ -103,7 +114,7 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
socket
|
socket
|
||||||
end
|
end
|
||||||
|
|
||||||
{:noreply, assign(socket, players: players)}
|
assign(socket, players: players)
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@@ -111,19 +122,20 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
~H"""
|
~H"""
|
||||||
<Layouts.app flash={@flash}>
|
<Layouts.app flash={@flash}>
|
||||||
<div class="min-h-screen -mx-4 -my-20 sm:-mx-6 lg:-mx-8">
|
<div class="min-h-screen -mx-4 -my-20 sm:-mx-6 lg:-mx-8">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<%!-- Header --%>
|
|
||||||
<div class="flex items-center justify-between mb-8">
|
<%!-- Player picker --%>
|
||||||
<div class="flex items-center gap-3">
|
<div :if={is_nil(@selected_player)}>
|
||||||
<div class="w-10 h-10 rounded-xl bg-primary/20 flex items-center justify-center">
|
<div class="flex items-center justify-between mb-8">
|
||||||
<.icon name="hero-cube-transparent" class="size-6 text-primary" />
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-primary/20 flex items-center justify-center">
|
||||||
|
<.icon name="hero-cube-transparent" class="size-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight text-base-content">Cobblemon</h1>
|
||||||
|
<p class="text-sm text-base-content/50">Who are you?</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-bold tracking-tight text-base-content">Cobblemon</h1>
|
|
||||||
<p class="text-sm text-base-content/50">Player Data Explorer</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button
|
<button
|
||||||
id="refresh-btn"
|
id="refresh-btn"
|
||||||
phx-click="refresh"
|
phx-click="refresh"
|
||||||
@@ -132,181 +144,158 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
<.icon name="hero-arrow-path" class="size-4" /> Refresh
|
<.icon name="hero-arrow-path" class="size-4" /> Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
:if={@players == []}
|
||||||
|
class="rounded-xl border border-base-300/30 bg-base-200/20 px-8 py-20 text-center"
|
||||||
|
>
|
||||||
|
<.icon name="hero-user-group" class="size-10 mx-auto mb-4 text-base-content/20" />
|
||||||
|
<p class="text-base-content/40 text-lg">No players found</p>
|
||||||
|
<p class="text-base-content/25 text-sm mt-1">Check the data directory</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
|
||||||
|
<.link
|
||||||
|
:for={player <- @players}
|
||||||
|
patch={~p"/player/#{player.uuid}"}
|
||||||
|
id={"player-#{player.uuid}"}
|
||||||
|
class="group flex items-center gap-3 rounded-xl border border-base-300/40 bg-base-200/20 px-5 py-4 hover:bg-base-200/50 hover:border-primary/30 hover:shadow-md hover:shadow-primary/5 transition-all duration-150"
|
||||||
|
>
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0 group-hover:bg-primary/20 transition-colors">
|
||||||
|
<.icon name="hero-user" class="size-5 text-primary/70" />
|
||||||
|
</div>
|
||||||
|
<span class="text-base font-semibold text-base-content/80 group-hover:text-base-content truncate">
|
||||||
|
{player.name || "Unknown"}
|
||||||
|
</span>
|
||||||
|
<.icon name="hero-chevron-right" class="size-4 text-base-content/20 ml-auto shrink-0 group-hover:text-primary/40 transition-colors" />
|
||||||
|
</.link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col lg:flex-row gap-6">
|
<%!-- Player view --%>
|
||||||
<%!-- Sidebar: Player List --%>
|
<div :if={@selected_player}>
|
||||||
<aside class="w-full lg:w-72 shrink-0">
|
<%!-- Header --%>
|
||||||
<div class="rounded-xl border border-base-300/50 bg-base-200/30 backdrop-blur-sm">
|
<div class="flex items-center justify-between mb-8">
|
||||||
<div class="px-4 py-3 border-b border-base-300/30">
|
<div class="flex items-center gap-3">
|
||||||
<h2 class="text-sm font-semibold text-base-content/70 uppercase tracking-wider">
|
<.link
|
||||||
Players
|
patch={~p"/"}
|
||||||
</h2>
|
id="back-to-players"
|
||||||
<p class="text-xs text-base-content/40 mt-0.5">
|
class="w-10 h-10 rounded-xl bg-base-200/50 hover:bg-base-200 flex items-center justify-center transition-colors"
|
||||||
{length(@players)} found
|
>
|
||||||
</p>
|
<.icon name="hero-arrow-left" class="size-5 text-base-content/50" />
|
||||||
</div>
|
</.link>
|
||||||
<div class="p-2 max-h-[60vh] overflow-y-auto">
|
<div>
|
||||||
<div
|
<h1 class="text-2xl font-bold tracking-tight text-base-content">
|
||||||
:if={@players == []}
|
{player_name(@players, @selected_player)}
|
||||||
class="px-3 py-8 text-center text-sm text-base-content/40"
|
</h1>
|
||||||
>
|
<p class="text-sm text-base-content/50">Player Data Explorer</p>
|
||||||
<.icon name="hero-user-group" class="size-8 mx-auto mb-2 opacity-30" />
|
|
||||||
<p>No players found</p>
|
|
||||||
<p class="text-xs mt-1">Check data directory</p>
|
|
||||||
</div>
|
|
||||||
<.link
|
|
||||||
:for={player <- @players}
|
|
||||||
patch={~p"/player/#{player.uuid}"}
|
|
||||||
class={[
|
|
||||||
"block px-3 py-2.5 rounded-lg transition-all duration-150 mb-1",
|
|
||||||
if(@selected_player == player.uuid,
|
|
||||||
do:
|
|
||||||
"bg-primary/15 text-primary border border-primary/20 shadow-sm shadow-primary/5",
|
|
||||||
else: "text-base-content/60 hover:bg-base-300/40 hover:text-base-content/80"
|
|
||||||
)
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<span class="text-sm font-medium block truncate">
|
|
||||||
{player.name || "Unknown"}
|
|
||||||
</span>
|
|
||||||
<span class="text-[10px] font-mono text-base-content/30 block truncate">
|
|
||||||
{player.uuid}
|
|
||||||
</span>
|
|
||||||
</.link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
<button
|
||||||
|
id="refresh-btn"
|
||||||
<%!-- Main Content --%>
|
phx-click="refresh"
|
||||||
<main class="flex-1 min-w-0">
|
class="btn btn-ghost btn-sm gap-2 hover:bg-base-300/50 transition-colors"
|
||||||
<div
|
|
||||||
:if={@error}
|
|
||||||
class="rounded-xl border border-error/30 bg-error/10 px-5 py-4 mb-6"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 text-error">
|
<.icon name="hero-arrow-path" class="size-4" /> Refresh
|
||||||
<.icon name="hero-exclamation-triangle" class="size-5" />
|
</button>
|
||||||
<span class="text-sm font-medium">{@error}</span>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
:if={@error}
|
||||||
|
class="rounded-xl border border-error/30 bg-error/10 px-5 py-4 mb-6"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 text-error">
|
||||||
|
<.icon name="hero-exclamation-triangle" class="size-5" />
|
||||||
|
<span class="text-sm font-medium">{@error}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :if={@player_data}>
|
||||||
|
<%!-- Stats bar --%>
|
||||||
|
<div class="flex items-center gap-4 text-sm text-base-content/50 mb-6">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-success/60" />
|
||||||
|
<span>{party_count(@player_data)} in party</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-info/60" />
|
||||||
|
<span>{pc_count(@player_data)} in PC</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%!-- Empty state --%>
|
<%!-- Active battle --%>
|
||||||
<div
|
<.battle_panel :if={@battle} battle={@battle} player_id={@selected_player} />
|
||||||
:if={is_nil(@selected_player)}
|
|
||||||
class="rounded-xl border border-base-300/30 bg-base-200/20 px-8 py-20 text-center"
|
<%!-- View mode tabs --%>
|
||||||
>
|
<div class="flex items-center gap-1 mb-5 p-1 rounded-lg bg-base-200/40 w-fit">
|
||||||
<.icon
|
<button
|
||||||
name="hero-arrow-left"
|
id="tab-party"
|
||||||
class="size-10 mx-auto mb-4 text-base-content/20"
|
phx-click="switch_view"
|
||||||
/>
|
phx-value-mode="party"
|
||||||
<p class="text-base-content/40 text-lg">Select a player to explore</p>
|
class={[
|
||||||
<p class="text-base-content/25 text-sm mt-1">
|
"px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-150",
|
||||||
Choose from the sidebar to view their Pokémon
|
if(@view_mode == :party,
|
||||||
</p>
|
do: "bg-base-100 text-base-content shadow-sm",
|
||||||
|
else: "text-base-content/50 hover:text-base-content/70"
|
||||||
|
)
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Party
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="tab-pc"
|
||||||
|
phx-click="switch_view"
|
||||||
|
phx-value-mode="pc"
|
||||||
|
class={[
|
||||||
|
"px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-150",
|
||||||
|
if(@view_mode == :pc,
|
||||||
|
do: "bg-base-100 text-base-content shadow-sm",
|
||||||
|
else: "text-base-content/50 hover:text-base-content/70"
|
||||||
|
)
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
PC Storage
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%!-- Player data --%>
|
<%!-- Party view --%>
|
||||||
<div :if={@player_data}>
|
<div :if={@view_mode == :party}>
|
||||||
<%!-- Player header --%>
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
<div class="rounded-xl border border-base-300/40 bg-base-200/30 px-5 py-4 mb-6">
|
<%= for {pokemon, idx} <- Enum.with_index(@player_data.party) do %>
|
||||||
<div class="flex items-center gap-3">
|
<.pokemon_card pokemon={pokemon} index={idx} />
|
||||||
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
<% end %>
|
||||||
<.icon name="hero-user" class="size-5 text-primary/70" />
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0">
|
|
||||||
<p class="text-base font-semibold text-base-content/90">
|
|
||||||
{player_name(@players, @player_data.uuid)}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs font-mono text-base-content/40 truncate">
|
|
||||||
{@player_data.uuid}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="ml-auto flex items-center gap-3 text-sm text-base-content/50">
|
|
||||||
<div class="flex items-center gap-1.5">
|
|
||||||
<div class="w-2 h-2 rounded-full bg-success/60" />
|
|
||||||
<span>{party_count(@player_data)} in party</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1.5">
|
|
||||||
<div class="w-2 h-2 rounded-full bg-info/60" />
|
|
||||||
<span>{pc_count(@player_data)} in PC</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<%!-- Active battle --%>
|
<%!-- PC view --%>
|
||||||
<.battle_panel :if={@battle} battle={@battle} player_id={@selected_player} />
|
<div :if={@view_mode == :pc}>
|
||||||
|
<div :for={box <- @player_data.pc} class="mb-6">
|
||||||
<%!-- View mode tabs --%>
|
<h3 class="text-sm font-semibold text-base-content/50 uppercase tracking-wider mb-3">
|
||||||
<div class="flex items-center gap-1 mb-5 p-1 rounded-lg bg-base-200/40 w-fit">
|
Box {box.box + 1}
|
||||||
<button
|
</h3>
|
||||||
id="tab-party"
|
<div class="grid grid-cols-3 sm:grid-cols-5 lg:grid-cols-6 gap-2">
|
||||||
phx-click="switch_view"
|
<%= for {pokemon, idx} <- Enum.with_index(box.pokemon) do %>
|
||||||
phx-value-mode="party"
|
<.pokemon_card
|
||||||
class={[
|
pokemon={pokemon}
|
||||||
"px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-150",
|
index={pc_global_index(@player_data.pc, box.box, idx)}
|
||||||
if(@view_mode == :party,
|
compact
|
||||||
do: "bg-base-100 text-base-content shadow-sm",
|
/>
|
||||||
else: "text-base-content/50 hover:text-base-content/70"
|
|
||||||
)
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
Party
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
id="tab-pc"
|
|
||||||
phx-click="switch_view"
|
|
||||||
phx-value-mode="pc"
|
|
||||||
class={[
|
|
||||||
"px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-150",
|
|
||||||
if(@view_mode == :pc,
|
|
||||||
do: "bg-base-100 text-base-content shadow-sm",
|
|
||||||
else: "text-base-content/50 hover:text-base-content/70"
|
|
||||||
)
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
PC Storage
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%!-- Party view --%>
|
|
||||||
<div :if={@view_mode == :party}>
|
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
|
||||||
<%= for {pokemon, idx} <- Enum.with_index(@player_data.party) do %>
|
|
||||||
<.pokemon_card pokemon={pokemon} index={idx} />
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
<%!-- PC view --%>
|
:if={@player_data.pc == []}
|
||||||
<div :if={@view_mode == :pc}>
|
class="text-center py-12 text-base-content/30 text-sm"
|
||||||
<div :for={box <- @player_data.pc} class="mb-6">
|
>
|
||||||
<h3 class="text-sm font-semibold text-base-content/50 uppercase tracking-wider mb-3">
|
PC storage is empty
|
||||||
Box {box.box + 1}
|
|
||||||
</h3>
|
|
||||||
<div class="grid grid-cols-3 sm:grid-cols-5 lg:grid-cols-6 gap-2">
|
|
||||||
<%= 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 %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
:if={@player_data.pc == []}
|
|
||||||
class="text-center py-12 text-base-content/30 text-sm"
|
|
||||||
>
|
|
||||||
PC storage is empty
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%!-- Pokémon detail panel --%>
|
|
||||||
<.pokemon_detail :if={@selected_pokemon} pokemon={@selected_pokemon} />
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
|
<%!-- Pokémon detail panel --%>
|
||||||
|
<.pokemon_detail :if={@selected_pokemon} pokemon={@selected_pokemon} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layouts.app>
|
</Layouts.app>
|
||||||
@@ -559,7 +548,6 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
<div class="px-5 py-3 border-b border-error/20 flex items-center gap-2">
|
<div class="px-5 py-3 border-b border-error/20 flex items-center gap-2">
|
||||||
<span class="w-2 h-2 rounded-full bg-error animate-pulse inline-block"></span>
|
<span class="w-2 h-2 rounded-full bg-error animate-pulse inline-block"></span>
|
||||||
<span class="text-sm font-semibold text-error">Active Battle</span>
|
<span class="text-sm font-semibold text-error">Active Battle</span>
|
||||||
<span class="ml-auto text-[10px] font-mono text-base-content/30">{@battle.battle_id}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-3 relative">
|
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-3 relative">
|
||||||
<div class="hidden md:flex absolute inset-y-0 left-1/2 -translate-x-1/2 items-center justify-center z-10 pointer-events-none">
|
<div class="hidden md:flex absolute inset-y-0 left-1/2 -translate-x-1/2 items-center justify-center z-10 pointer-events-none">
|
||||||
|
|||||||
Reference in New Issue
Block a user