in same ui
All checks were successful
Build and Deploy / Build & Push Image (push) Successful in 36s
All checks were successful
Build and Deploy / Build & Push Image (push) Successful in 36s
This commit is contained in:
@@ -1,243 +0,0 @@
|
|||||||
defmodule CobblemonUiWeb.BattlesLive do
|
|
||||||
use CobblemonUiWeb, :live_view
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def mount(_params, _session, socket) do
|
|
||||||
{battles, error} =
|
|
||||||
case CobblemonUi.BattlesApi.list_battles() do
|
|
||||||
{:ok, battles} -> {battles, nil}
|
|
||||||
{:error, reason} -> {[], reason}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok,
|
|
||||||
assign(socket,
|
|
||||||
page_title: "Active Battles",
|
|
||||||
battles: battles,
|
|
||||||
error: error
|
|
||||||
)}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("refresh", _params, socket) do
|
|
||||||
{battles, error} =
|
|
||||||
case CobblemonUi.BattlesApi.list_battles() do
|
|
||||||
{:ok, battles} -> {battles, nil}
|
|
||||||
{:error, reason} -> {[], reason}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, assign(socket, battles: battles, error: error)}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<Layouts.app flash={@flash}>
|
|
||||||
<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">
|
|
||||||
<%!-- Header --%>
|
|
||||||
<div class="flex items-center justify-between mb-8">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-10 h-10 rounded-xl bg-error/20 flex items-center justify-center">
|
|
||||||
<.icon name="hero-bolt" class="size-6 text-error" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-bold tracking-tight text-base-content">Active Battles</h1>
|
|
||||||
<p class="text-sm text-base-content/50">Live battle monitor</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<.link
|
|
||||||
navigate={~p"/"}
|
|
||||||
class="btn btn-ghost btn-sm gap-2 hover:bg-base-300/50 transition-colors"
|
|
||||||
>
|
|
||||||
<.icon name="hero-arrow-left" class="size-4" /> Players
|
|
||||||
</.link>
|
|
||||||
<button
|
|
||||||
id="refresh-btn"
|
|
||||||
phx-click="refresh"
|
|
||||||
class="btn btn-ghost btn-sm gap-2 hover:bg-base-300/50 transition-colors"
|
|
||||||
>
|
|
||||||
<.icon name="hero-arrow-path" class="size-4" /> Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%!-- Error --%>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<%!-- Empty state --%>
|
|
||||||
<div
|
|
||||||
:if={@battles == [] && is_nil(@error)}
|
|
||||||
class="rounded-xl border border-base-300/30 bg-base-200/20 px-8 py-20 text-center"
|
|
||||||
>
|
|
||||||
<.icon name="hero-shield-check" class="size-10 mx-auto mb-4 text-base-content/20" />
|
|
||||||
<p class="text-base-content/40 text-lg">No active battles</p>
|
|
||||||
<p class="text-base-content/25 text-sm mt-1">All is peaceful on the server</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%!-- Battle cards --%>
|
|
||||||
<div id="battles" class="space-y-6">
|
|
||||||
<%= for battle <- @battles do %>
|
|
||||||
<div
|
|
||||||
id={"battle-#{battle.battle_id}"}
|
|
||||||
class="rounded-xl border border-base-300/40 bg-base-200/30 backdrop-blur-sm overflow-hidden"
|
|
||||||
>
|
|
||||||
<%!-- Battle header --%>
|
|
||||||
<div class="px-5 py-3 border-b border-base-300/30 flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="inline-flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-error bg-error/10 px-2.5 py-1 rounded-full">
|
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-error animate-pulse inline-block"></span>
|
|
||||||
Live
|
|
||||||
</span>
|
|
||||||
<span class="text-xs font-mono text-base-content/30">{battle.battle_id}</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-xs text-base-content/40">
|
|
||||||
{length(battle.actors)} combatants
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%!-- Actors --%>
|
|
||||||
<div class="p-5 grid grid-cols-1 md:grid-cols-2 gap-4 relative">
|
|
||||||
<%!-- VS divider on md+ --%>
|
|
||||||
<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">
|
|
||||||
<span class="text-lg font-black text-base-content/20 bg-base-200/80 px-2 py-1 rounded-lg">
|
|
||||||
VS
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= for actor <- battle.actors do %>
|
|
||||||
<div class={[
|
|
||||||
"rounded-xl border p-4",
|
|
||||||
if(actor.type == "player",
|
|
||||||
do: "border-primary/25 bg-primary/5",
|
|
||||||
else: "border-warning/25 bg-warning/5"
|
|
||||||
)
|
|
||||||
]}>
|
|
||||||
<%!-- Actor header --%>
|
|
||||||
<div class="flex items-center gap-2 mb-3">
|
|
||||||
<div class={[
|
|
||||||
"w-8 h-8 rounded-lg flex items-center justify-center shrink-0",
|
|
||||||
if(actor.type == "player",
|
|
||||||
do: "bg-primary/15",
|
|
||||||
else: "bg-warning/15"
|
|
||||||
)
|
|
||||||
]}>
|
|
||||||
<%= if actor.type == "player" do %>
|
|
||||||
<.icon name="hero-user" class="size-4 text-primary" />
|
|
||||||
<% else %>
|
|
||||||
<.icon name="hero-cpu-chip" class="size-4 text-warning" />
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-semibold text-base-content">{actor.name}</p>
|
|
||||||
<p class={[
|
|
||||||
"text-[10px] uppercase font-medium tracking-wide",
|
|
||||||
if(actor.type == "player",
|
|
||||||
do: "text-primary/60",
|
|
||||||
else: "text-warning/60"
|
|
||||||
)
|
|
||||||
]}>
|
|
||||||
{actor.type}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%!-- Active Pokémon --%>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<%= for poke <- actor.active_pokemon do %>
|
|
||||||
<div class="rounded-lg bg-base-100/50 border border-base-300/30 px-3 py-2.5">
|
|
||||||
<div class="flex items-center justify-between mb-1.5">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-sm font-bold text-base-content">{poke.species}</span>
|
|
||||||
<span class="text-[10px] text-base-content/40 bg-base-300/40 px-1.5 py-0.5 rounded-md font-mono">
|
|
||||||
Lv.{poke.level}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-xs text-base-content/50 font-mono">
|
|
||||||
{poke.hp}/{poke.max_hp}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<%!-- HP bar --%>
|
|
||||||
<div class="w-full h-2 rounded-full bg-base-300/50 overflow-hidden">
|
|
||||||
<div
|
|
||||||
class={[
|
|
||||||
"h-full rounded-full transition-all duration-300",
|
|
||||||
cond do
|
|
||||||
poke.max_hp == 0 -> "bg-base-content/20"
|
|
||||||
poke.hp / poke.max_hp > 0.5 -> "bg-success"
|
|
||||||
poke.hp / poke.max_hp > 0.2 -> "bg-warning"
|
|
||||||
true -> "bg-error"
|
|
||||||
end
|
|
||||||
]}
|
|
||||||
style={"width: #{if poke.max_hp > 0, do: Float.round(poke.hp / poke.max_hp * 100, 1), else: 0}%"}
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%!-- Party moves (player only) --%>
|
|
||||||
<%= if actor.party != [] do %>
|
|
||||||
<details class="mt-3 group">
|
|
||||||
<summary class="text-xs text-base-content/40 cursor-pointer hover:text-base-content/60 transition-colors select-none list-none flex items-center gap-1">
|
|
||||||
<.icon name="hero-chevron-right" class="size-3 group-open:rotate-90 transition-transform" />
|
|
||||||
Party details
|
|
||||||
</summary>
|
|
||||||
<div class="mt-2 space-y-2">
|
|
||||||
<%= for party_poke <- actor.party do %>
|
|
||||||
<div class="rounded-lg bg-base-100/30 border border-base-300/20 px-3 py-2">
|
|
||||||
<div class="flex items-center justify-between mb-1">
|
|
||||||
<div class="flex items-center gap-1.5">
|
|
||||||
<span class="text-xs font-semibold text-base-content">
|
|
||||||
{party_poke.species}
|
|
||||||
</span>
|
|
||||||
<%= if party_poke.shiny do %>
|
|
||||||
<span class="text-[9px] bg-yellow-400/20 text-yellow-500 px-1 rounded font-semibold">✦ Shiny</span>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<span class="text-[10px] font-mono text-base-content/40">
|
|
||||||
Lv.{party_poke.level}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-1.5 text-[10px] text-base-content/50 mb-1.5">
|
|
||||||
<span :if={party_poke.ability} class="bg-base-300/30 px-1.5 py-0.5 rounded">
|
|
||||||
{party_poke.ability}
|
|
||||||
</span>
|
|
||||||
<span :if={party_poke.nature} class="bg-base-300/30 px-1.5 py-0.5 rounded">
|
|
||||||
{party_poke.nature |> String.replace("cobblemon:", "")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-1">
|
|
||||||
<%= for move <- party_poke.moves do %>
|
|
||||||
<span class="inline-flex items-center gap-1 text-[10px] bg-base-300/30 text-base-content/60 px-2 py-0.5 rounded-full">
|
|
||||||
{move.name}
|
|
||||||
<span class="text-base-content/30">{move.pp}/{move.max_pp}</span>
|
|
||||||
</span>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layouts.app>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -14,6 +14,7 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
players: players,
|
players: players,
|
||||||
selected_player: nil,
|
selected_player: nil,
|
||||||
player_data: nil,
|
player_data: nil,
|
||||||
|
battle: nil,
|
||||||
selected_pokemon: nil,
|
selected_pokemon: nil,
|
||||||
view_mode: :party,
|
view_mode: :party,
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -25,10 +26,13 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
def handle_params(%{"uuid" => uuid}, _uri, socket) do
|
def handle_params(%{"uuid" => uuid}, _uri, socket) do
|
||||||
case CobblemonUi.CobblemonFS.get_player(uuid) do
|
case CobblemonUi.CobblemonFS.get_player(uuid) do
|
||||||
{:ok, data} ->
|
{:ok, data} ->
|
||||||
|
battle = find_player_battle(uuid)
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
assign(socket,
|
assign(socket,
|
||||||
selected_player: uuid,
|
selected_player: uuid,
|
||||||
player_data: data,
|
player_data: data,
|
||||||
|
battle: battle,
|
||||||
selected_pokemon: nil,
|
selected_pokemon: nil,
|
||||||
error: nil
|
error: nil
|
||||||
)}
|
)}
|
||||||
@@ -52,7 +56,7 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_params(_params, _uri, socket) do
|
def handle_params(_params, _uri, socket) do
|
||||||
{:noreply, assign(socket, selected_player: nil, player_data: nil, selected_pokemon: nil)}
|
{:noreply, assign(socket, selected_player: nil, player_data: nil, battle: nil, selected_pokemon: nil)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@@ -89,9 +93,11 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
|
|
||||||
socket =
|
socket =
|
||||||
if uuid = socket.assigns.selected_player do
|
if uuid = socket.assigns.selected_player do
|
||||||
|
battle = find_player_battle(uuid)
|
||||||
|
|
||||||
case CobblemonUi.CobblemonFS.get_player(uuid) do
|
case CobblemonUi.CobblemonFS.get_player(uuid) do
|
||||||
{:ok, data} -> assign(socket, player_data: data, error: nil)
|
{:ok, data} -> assign(socket, player_data: data, battle: battle, error: nil)
|
||||||
_ -> socket
|
_ -> assign(socket, battle: battle)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
socket
|
socket
|
||||||
@@ -118,12 +124,6 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<.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
|
|
||||||
</.link>
|
|
||||||
<button
|
<button
|
||||||
id="refresh-btn"
|
id="refresh-btn"
|
||||||
phx-click="refresh"
|
phx-click="refresh"
|
||||||
@@ -234,6 +234,9 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%!-- Active battle --%>
|
||||||
|
<.battle_panel :if={@battle} battle={@battle} player_id={@selected_player} />
|
||||||
|
|
||||||
<%!-- View mode tabs --%>
|
<%!-- View mode tabs --%>
|
||||||
<div class="flex items-center gap-1 mb-5 p-1 rounded-lg bg-base-200/40 w-fit">
|
<div class="flex items-center gap-1 mb-5 p-1 rounded-lg bg-base-200/40 w-fit">
|
||||||
<button
|
<button
|
||||||
@@ -545,6 +548,88 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# --- Battle components ---
|
||||||
|
|
||||||
|
attr :battle, :map, required: true
|
||||||
|
attr :player_id, :string, required: true
|
||||||
|
|
||||||
|
defp battle_panel(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="rounded-xl border border-error/30 bg-error/5 overflow-hidden mb-6">
|
||||||
|
<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="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 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">
|
||||||
|
<span class="text-base font-black text-base-content/20 bg-base-200/80 px-2 py-1 rounded-lg">
|
||||||
|
VS
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<%= for actor <- @battle.actors do %>
|
||||||
|
<div class={[
|
||||||
|
"rounded-lg border p-3",
|
||||||
|
if(actor.player_id == @player_id,
|
||||||
|
do: "border-primary/25 bg-primary/5",
|
||||||
|
else: "border-warning/25 bg-warning/5"
|
||||||
|
)
|
||||||
|
]}>
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<div class={[
|
||||||
|
"w-7 h-7 rounded-md flex items-center justify-center shrink-0",
|
||||||
|
if(actor.type == "player", do: "bg-primary/15", else: "bg-warning/15")
|
||||||
|
]}>
|
||||||
|
<%= if actor.type == "player" do %>
|
||||||
|
<.icon name="hero-user" class="size-3.5 text-primary" />
|
||||||
|
<% else %>
|
||||||
|
<.icon name="hero-cpu-chip" class="size-3.5 text-warning" />
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold text-base-content">{actor.name}</p>
|
||||||
|
<p class={[
|
||||||
|
"text-[10px] uppercase font-medium tracking-wide",
|
||||||
|
if(actor.type == "player", do: "text-primary/60", else: "text-warning/60")
|
||||||
|
]}>
|
||||||
|
{actor.type}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%= for poke <- actor.active_pokemon do %>
|
||||||
|
<div class="rounded-md bg-base-100/50 border border-base-300/30 px-3 py-2">
|
||||||
|
<div class="flex items-center justify-between mb-1">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span class="text-sm font-bold text-base-content">{poke.species}</span>
|
||||||
|
<span class="text-[10px] text-base-content/40 bg-base-300/40 px-1.5 py-0.5 rounded font-mono">
|
||||||
|
Lv.{poke.level}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-mono text-base-content/50">{poke.hp}/{poke.max_hp}</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-1.5 rounded-full bg-base-300/50 overflow-hidden">
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
"h-full rounded-full transition-all",
|
||||||
|
cond do
|
||||||
|
poke.max_hp == 0 -> "bg-base-content/20"
|
||||||
|
poke.hp / poke.max_hp > 0.5 -> "bg-success"
|
||||||
|
poke.hp / poke.max_hp > 0.2 -> "bg-warning"
|
||||||
|
true -> "bg-error"
|
||||||
|
end
|
||||||
|
]}
|
||||||
|
style={"width: #{if poke.max_hp > 0, do: Float.round(poke.hp / poke.max_hp * 100, 1), else: 0}%"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
# --- Helpers ---
|
# --- Helpers ---
|
||||||
|
|
||||||
defp party_count(%{party: party}), do: Enum.count(party, &(not is_nil(&1)))
|
defp party_count(%{party: party}), do: Enum.count(party, &(not is_nil(&1)))
|
||||||
@@ -599,4 +684,16 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
_ -> "Unknown"
|
_ -> "Unknown"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp find_player_battle(uuid) do
|
||||||
|
case CobblemonUi.BattlesApi.list_battles() do
|
||||||
|
{:ok, battles} ->
|
||||||
|
Enum.find(battles, fn battle ->
|
||||||
|
Enum.any?(battle.actors, fn actor -> actor.player_id == uuid end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ defmodule CobblemonUiWeb.Router do
|
|||||||
|
|
||||||
live "/", DashboardLive
|
live "/", DashboardLive
|
||||||
live "/player/:uuid", DashboardLive
|
live "/player/:uuid", DashboardLive
|
||||||
live "/battles", BattlesLive
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
# Other scopes may use custom stacks.
|
||||||
|
|||||||
Reference in New Issue
Block a user