basic admin endgame view and add
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
defmodule ChesstrainerWeb.Board.BoardComponent do
|
||||
@moduledoc """
|
||||
Chess board component
|
||||
"""
|
||||
|
||||
use Phoenix.Component
|
||||
import ChesstrainerWeb.Board.SquareComponent
|
||||
|
||||
attr :game, :map, required: true
|
||||
|
||||
attr :myself, :any, required: true
|
||||
|
||||
def board(assigns) do
|
||||
files_list = [:a, :b, :c, :d, :e, :f, :g, :h]
|
||||
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:files_list, files_list)
|
||||
|> assign(
|
||||
:files,
|
||||
case assigns.game.orientation do
|
||||
:white -> files_list
|
||||
:black -> Enum.reverse(files_list)
|
||||
end
|
||||
)
|
||||
|> assign(
|
||||
:ranks,
|
||||
case assigns.game.orientation do
|
||||
:white -> Enum.reverse(1..8)
|
||||
:black -> 1..8
|
||||
end
|
||||
)
|
||||
|
||||
~H"""
|
||||
<div class="w-[388px] m-auto left-0 right-0 mt-6">
|
||||
<div class="border border-2 border-zinc-700 grid grid-rows-8 grid-cols-8">
|
||||
<%= for rank <- @ranks do %>
|
||||
<%= for file <- @files do %>
|
||||
<% square = {file, rank} %>
|
||||
<% piece = Map.get(@game.board, square) %>
|
||||
<.square
|
||||
file={file}
|
||||
rank={rank}
|
||||
piece={piece}
|
||||
files_list={@files_list}
|
||||
move_from_square={@game.move_from_square}
|
||||
myself={@myself}
|
||||
/>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
defmodule ChesstrainerWeb.Board.PieceComponent do
|
||||
@moduledoc """
|
||||
Chess pieces component
|
||||
"""
|
||||
use Phoenix.Component
|
||||
|
||||
attr :piece, :any, required: true
|
||||
attr :class, :string, default: ""
|
||||
|
||||
def piece(assigns) do
|
||||
assigns = assign(assigns, :path, piece_path(assigns.piece))
|
||||
|
||||
~H"""
|
||||
<%= if @path do %>
|
||||
<img src={@path} class={@class} />
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
def piece_path({:king, :white, _square}), do: "/images/pieces/white/king.svg"
|
||||
def piece_path({:queen, :white, _square}), do: "/images/pieces/white/queen.svg"
|
||||
def piece_path({:rook, :white, _square}), do: "/images/pieces/white/rook.svg"
|
||||
def piece_path({:bishop, :white, _square}), do: "/images/pieces/white/bishop.svg"
|
||||
def piece_path({:knight, :white, _square}), do: "/images/pieces/white/knight.svg"
|
||||
def piece_path({:pawn, :white, _square}), do: "/images/pieces/white/pawn.svg"
|
||||
def piece_path({:king, :black, _square}), do: "/images/pieces/black/king.svg"
|
||||
def piece_path({:queen, :black, _square}), do: "/images/pieces/black/queen.svg"
|
||||
def piece_path({:rook, :black, _square}), do: "/images/pieces/black/rook.svg"
|
||||
def piece_path({:bishop, :black, _square}), do: "/images/pieces/black/bishop.svg"
|
||||
def piece_path({:knight, :black, _square}), do: "/images/pieces/black/knight.svg"
|
||||
def piece_path({:pawn, :black, _square}), do: "/images/pieces/black/pawn.svg"
|
||||
def piece_path(_), do: nil
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
defmodule ChesstrainerWeb.Board.SquareComponent do
|
||||
@moduledoc """
|
||||
Renders a single chess square with optional piece and highlighting.
|
||||
"""
|
||||
use Phoenix.Component
|
||||
import ChesstrainerWeb.Board.PieceComponent
|
||||
|
||||
attr :file, :atom, required: true
|
||||
attr :rank, :integer, required: true
|
||||
attr :piece, :any, default: nil
|
||||
attr :files_list, :list, required: true
|
||||
attr :move_from_square, :any, default: nil
|
||||
attr :myself, :any, required: true
|
||||
|
||||
def square(assigns) do
|
||||
current_square = {assigns.file, assigns.rank}
|
||||
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:highlight_square, current_square === assigns.move_from_square)
|
||||
|
||||
~H"""
|
||||
<div
|
||||
class={[
|
||||
"w-12 h-12 flex items-center justify-center",
|
||||
background(
|
||||
Enum.find_index(@files_list, fn f -> f == @file end),
|
||||
@rank - 1
|
||||
),
|
||||
highlight_square(@highlight_square)
|
||||
]}
|
||||
phx-click="square-click"
|
||||
phx-value-file={@file}
|
||||
phx-value-rank={@rank}
|
||||
phx-value-type="move"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<.piece piece={@piece} class="w-10 h-10" />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def background(file_idx, rank_idx) when rem(file_idx + rank_idx, 2) != 0, do: "bg-boardwhite"
|
||||
def background(_, _), do: "bg-boardblack"
|
||||
|
||||
def highlight_square(true), do: "ring-inset ring-2 ring-yellow-400"
|
||||
def highlight_square(false), do: ""
|
||||
end
|
||||
@@ -0,0 +1,212 @@
|
||||
defmodule ChesstrainerWeb.EndgameLive.Form do
|
||||
use ChesstrainerWeb, :live_view
|
||||
|
||||
alias Chesstrainer.Repo
|
||||
alias Chesstrainer.Tags
|
||||
alias Chesstrainer.FEN
|
||||
alias Chesstrainer.Endgames
|
||||
alias Chesstrainer.Endgames.Endgame
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>Use this form to manage endgame records in your database.</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.form for={@form} id="endgame-form" phx-change="validate" phx-submit="save">
|
||||
<.input field={@form[:fen]} type="text" label="Fen" disabled={@live_action == :edit} />
|
||||
<.input field={@form[:color]} type="text" label="Color" disabled />
|
||||
<.input field={@form[:key]} type="text" label="Key" />
|
||||
<.input field={@form[:rating]} type="number" label="Rating" />
|
||||
|
||||
<!-- ✅ TAG SELECTOR -->
|
||||
<div class="field">
|
||||
<label>Tags</label>
|
||||
|
||||
<!-- Search box -->
|
||||
<input
|
||||
type="text"
|
||||
name="tag_search"
|
||||
value={@tag_query}
|
||||
phx-change="search_tags"
|
||||
autocomplete="off"
|
||||
/>
|
||||
|
||||
<!-- Search results -->
|
||||
<ul class="tag-results">
|
||||
<%= for tag <- @tag_results do %>
|
||||
<li phx-click="add_tag" phx-value-id={tag.id}>
|
||||
{tag.name}
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
<%= if @tag_query != "" and @tag_results == [] do %>
|
||||
<li phx-click="create_tag" phx-value-name={@tag_query}>
|
||||
Create tag "{@tag_query}"
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<!-- Selected tags -->
|
||||
<div class="selected-tags">
|
||||
<%= for tag <- @endgame.tags do %>
|
||||
<span class="tag">
|
||||
{tag.name}
|
||||
<button type="button" phx-click="remove_tag" phx-value-id={tag.id}>×</button>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.input field={@form[:message]} type="textarea" label="Message" />
|
||||
<.input field={@form[:notes]} type="textarea" label="Notes" />
|
||||
<.input
|
||||
field={@form[:result]}
|
||||
type="select"
|
||||
options={[
|
||||
{"Win", :win},
|
||||
{"Draw", :draw},
|
||||
{"Loss", :loss}
|
||||
]}
|
||||
label="Result"
|
||||
/>
|
||||
<footer>
|
||||
<.button phx-disable-with="Saving..." variant="primary">Save Endgame</.button>
|
||||
<.button navigate={return_path(@return_to, @endgame)}>Cancel</.button>
|
||||
</footer>
|
||||
</.form>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:return_to, return_to(params["return_to"]))
|
||||
|> apply_action(socket.assigns.live_action, params)}
|
||||
end
|
||||
|
||||
defp return_to("show"), do: "show"
|
||||
defp return_to(_), do: "index"
|
||||
|
||||
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||
endgame = Endgames.get_endgame!(id)
|
||||
|
||||
socket
|
||||
|> assign(:page_title, "Edit Endgame")
|
||||
|> assign(:endgame, endgame)
|
||||
|> assign(:tag_query, "")
|
||||
|> assign(:tag_results, [])
|
||||
|> assign(:endgame, Repo.preload(endgame, :tags))
|
||||
|> assign(:form, to_form(Endgames.change_endgame(endgame)))
|
||||
end
|
||||
|
||||
defp apply_action(socket, :new, _params) do
|
||||
endgame = %Endgame{}
|
||||
|
||||
socket
|
||||
|> assign(:page_title, "New Endgame")
|
||||
|> assign(:endgame, endgame)
|
||||
|> assign(:tag_query, "")
|
||||
|> assign(:tag_results, [])
|
||||
|> assign(:endgame, Repo.preload(endgame, :tags))
|
||||
|> assign(:form, to_form(Endgames.change_endgame(endgame)))
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("search_tags", %{"tag_search" => query}, socket) do
|
||||
tags = Tags.search(query, "endgame")
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:tag_query, query)
|
||||
|> assign(:tag_results, tags)}
|
||||
end
|
||||
|
||||
def handle_event("add_tag", %{"id" => id}, socket) do
|
||||
tag = Tags.get_tag!(id)
|
||||
{:ok, endgame} = Endgames.add_tag(socket.assigns.endgame, tag)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:endgame, Repo.preload(endgame, :tags))
|
||||
|> assign(:tag_query, "")
|
||||
|> assign(:tag_results, [])}
|
||||
end
|
||||
|
||||
def handle_event("create_tag", %{"name" => name}, socket) do
|
||||
{:ok, tag} = Tags.get_or_create(name, "endgame")
|
||||
{:ok, endgame} = Endgames.add_tag(socket.assigns.endgame, tag)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:endgame, Repo.preload(endgame, :tags))
|
||||
|> assign(:tag_query, "")
|
||||
|> assign(:tag_results, [])}
|
||||
end
|
||||
|
||||
def handle_event("remove_tag", %{"id" => id}, socket) do
|
||||
tag = Tags.get_tag!(id)
|
||||
{:ok, endgame} = Endgames.remove_tag(socket.assigns.endgame, tag)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:endgame, Repo.preload(endgame, :tags))}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"endgame" => endgame_params}, socket) do
|
||||
# Compute color from the incoming FEN
|
||||
color = FEN.color_from_fen(endgame_params["fen"])
|
||||
|
||||
# Update the struct
|
||||
endgame = %{socket.assigns.endgame | color: color}
|
||||
|
||||
# Build the changeset *from the updated struct*
|
||||
changeset =
|
||||
endgame
|
||||
|> Endgames.change_endgame()
|
||||
|> Ecto.Changeset.put_change(:color, color)
|
||||
|
||||
{:noreply, assign(socket, endgame: endgame, form: to_form(changeset, action: :validate))}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"endgame" => endgame_params}, socket) do
|
||||
save_endgame(socket, socket.assigns.live_action, endgame_params)
|
||||
end
|
||||
|
||||
defp save_endgame(socket, :edit, endgame_params) do
|
||||
case Endgames.update_endgame(socket.assigns.endgame, endgame_params) do
|
||||
{:ok, endgame} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Endgame updated successfully")
|
||||
|> push_navigate(to: return_path(socket.assigns.return_to, endgame))}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, form: to_form(changeset))}
|
||||
end
|
||||
end
|
||||
|
||||
defp save_endgame(socket, :new, endgame_params) do
|
||||
color = FEN.color_from_fen(endgame_params["fen"])
|
||||
params = Map.put(endgame_params, "color", color)
|
||||
|
||||
case Endgames.create_endgame(params) do
|
||||
{:ok, endgame} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Endgame created successfully")
|
||||
|> push_navigate(to: return_path(socket.assigns.return_to, endgame))}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, form: to_form(changeset))}
|
||||
end
|
||||
end
|
||||
|
||||
defp return_path("index", _endgame), do: ~p"/admin/endgames"
|
||||
defp return_path("show", endgame), do: ~p"/admin/endgames/#{endgame}"
|
||||
end
|
||||
@@ -0,0 +1,71 @@
|
||||
defmodule ChesstrainerWeb.EndgameLive.Index do
|
||||
use ChesstrainerWeb, :live_view
|
||||
|
||||
alias Chesstrainer.Endgames
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
Listing Endgames
|
||||
<:actions>
|
||||
<.button variant="primary" navigate={~p"/admin/endgames/new"}>
|
||||
<.icon name="hero-plus" /> New Endgame
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.table
|
||||
id="endgames"
|
||||
rows={@streams.endgames}
|
||||
row_click={fn {_id, endgame} -> JS.navigate(~p"/admin/endgames/#{endgame}") end}
|
||||
>
|
||||
<:col :let={{_id, endgame}} label="Fen">{endgame.fen}</:col>
|
||||
<:col :let={{_id, endgame}} label="Color">{endgame.color}</:col>
|
||||
<:col :let={{_id, endgame}} label="Key">{endgame.key}</:col>
|
||||
<:col :let={{_id, endgame}} label="Rating">{endgame.rating}</:col>
|
||||
<:col :let={{_id, endgame}} label="Message">{endgame.message}</:col>
|
||||
<:col :let={{_id, endgame}} label="Notes">{endgame.notes}</:col>
|
||||
<:col :let={{_id, endgame}} label="Result">{endgame.result}</:col>
|
||||
<:col :let={{_id, endgame}} label="Attempted">{endgame.times_attempted}</:col>
|
||||
<:col :let={{_id, endgame}} label="Solved">{endgame.times_solved}</:col>
|
||||
<:action :let={{_id, endgame}}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/admin/endgames/#{endgame}"}>Show</.link>
|
||||
</div>
|
||||
<.link navigate={~p"/admin/endgames/#{endgame}/edit"}>Edit</.link>
|
||||
</:action>
|
||||
<:action :let={{id, endgame}}>
|
||||
<.link
|
||||
phx-click={JS.push("delete", value: %{id: endgame.id}) |> hide("##{id}")}
|
||||
data-confirm="Are you sure?"
|
||||
>
|
||||
Delete
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Listing Endgames")
|
||||
|> stream(:endgames, list_endgames())}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
endgame = Endgames.get_endgame!(id)
|
||||
{:ok, _} = Endgames.delete_endgame(endgame)
|
||||
|
||||
{:noreply, stream_delete(socket, :endgames, endgame)}
|
||||
end
|
||||
|
||||
defp list_endgames() do
|
||||
Endgames.list_endgames()
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
defmodule ChesstrainerWeb.EndgameLive.Show do
|
||||
use ChesstrainerWeb, :live_view
|
||||
|
||||
alias Chesstrainer.Endgames
|
||||
alias Chesstrainer.Game
|
||||
|
||||
import ChesstrainerWeb.Board.BoardComponent
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
game =
|
||||
case Game.game_from_fen(assigns.endgame.fen, :endgame) do
|
||||
{:ok, g} -> g
|
||||
{:error, :invalid_fen, g} -> g
|
||||
end
|
||||
|
||||
# TODO once we return {:error, :invalid_fen, game}, then put flash to show it is invalid
|
||||
# write tests for this
|
||||
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:game, game)
|
||||
|> assign(:game_type, :endgame)
|
||||
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
Endgame {@endgame.id}
|
||||
<:subtitle>This is a endgame record from your database.</:subtitle>
|
||||
<:actions>
|
||||
<.button navigate={~p"/admin/endgames"}>
|
||||
<.icon name="hero-arrow-left" />
|
||||
</.button>
|
||||
<.button variant="primary" navigate={~p"/admin/endgames/#{@endgame}/edit?return_to=show"}>
|
||||
<.icon name="hero-pencil-square" /> Edit endgame
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.board game={@game} myself="board" />
|
||||
|
||||
<.list>
|
||||
<:item title="Fen">{@endgame.fen}</:item>
|
||||
<:item title="Key">{@endgame.key}</:item>
|
||||
<:item title="Color">{@endgame.color}</:item>
|
||||
<:item title="Rating">{@endgame.rating}</:item>
|
||||
<:item title="Message">{@endgame.message}</:item>
|
||||
<:item title="Notes">{@endgame.notes}</:item>
|
||||
<:item title="Result">{@endgame.result}</:item>
|
||||
<:item title="Attempted">{@endgame.times_attempted}</:item>
|
||||
<:item title="Solved">{@endgame.times_solved}</:item>
|
||||
</.list>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(%{"id" => id}, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Show Endgame")
|
||||
|> assign(:endgame, Endgames.get_endgame!(id))}
|
||||
end
|
||||
end
|
||||
@@ -22,6 +22,17 @@ defmodule ChesstrainerWeb.Router do
|
||||
live "/chess-test", ChessTestLive
|
||||
end
|
||||
|
||||
scope "/admin", ChesstrainerWeb do
|
||||
pipe_through :browser
|
||||
|
||||
live_session :endgames do
|
||||
live "/endgames", EndgameLive.Index, :index
|
||||
live "/endgames/new", EndgameLive.Form, :new
|
||||
live "/endgames/:id", EndgameLive.Show, :show
|
||||
live "/endgames/:id/edit", EndgameLive.Form, :edit
|
||||
end
|
||||
end
|
||||
|
||||
# Other scopes may use custom stacks.
|
||||
# scope "/api", ChesstrainerWeb do
|
||||
# pipe_through :api
|
||||
|
||||
Reference in New Issue
Block a user