refactor champions, position filters, champion card...
All checks were successful
ci / docker (push) Successful in 3m41s

This commit is contained in:
Álvaro 2024-06-08 23:16:53 +02:00
parent 2b51aace6e
commit d9dee3af7b
30 changed files with 321 additions and 202 deletions

View File

@ -45,13 +45,10 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
|> IO.inspect()
end
@spec get_champion_spells_by_win_rate(String.t()) :: list()
def get_champion_spells_by_win_rate(championId) do
end
def get_champion_picked_summoners() do
def get_champion_picked_summoners(championId, team_position \\ "MIDDLE") do
query =
from f in Schema,
where: f.champion_id == ^championId and f.team_position == ^team_position,
join: c in ChampionSchema,
on: c.champion_id == f.champion_id,
join: s in SummonerSpellSchema,
@ -70,7 +67,6 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
metadata: s.metadata,
champion_name: c.name,
champion_id: c.champion_id,
image: c.image,
team_position: f.team_position,
total_games: count("*")
},

View File

@ -0,0 +1,16 @@
defmodule LolAnalyticsWeb.ChampionComponents.ChampionAvatar do
use Phoenix.Component
attr :id, :integer, required: true
attr :image, :string, required: true
attr :name, :string, required: true
def champion_avatar(assigns) do
~H"""
<div class="flex flex-col w-40">
<img src={@image} alt="champion-icon" />
<p class="w-full text-center"><%= @name %></p>
</div>
"""
end
end

View File

@ -0,0 +1,33 @@
defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard.Props do
defstruct [:id, :win_rate, :image, :name, :team_position, :wins, :total_games]
end
defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard do
use Phoenix.Component
alias LolAnalyticsWeb.ChampionComponents.ChampionCard.Props
attr :props, Props, default: %Props{}
def champion_card(assigns) do
~H"""
<.link patch={"/champions/#{@props.id}"}>
<div class="flex flex-col rounded-xl bg-clip-border overflow-hidden bg-gray-200">
<img src={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{@props.image}"} />
<div class="py-2" />
<div class="flex mx-auto">
<img src={team_position_image(@props.team_position)} class="w-5 h-5" />
<h3><%= @props.name %></h3>
</div>
<div class="py-2" />
</div>
</.link>
"""
end
defp team_position_image("BOTTOM"), do: "/images/lanes/bot.png"
defp team_position_image("MIDDLE"), do: "/images/lanes/mid.png"
defp team_position_image("TOP"), do: "/images/lanes/top.png"
defp team_position_image("JUNGLE"), do: "/images/lanes/jungle.png"
defp team_position_image("UTILITY"), do: "/images/lanes/utility.png"
end

View File

@ -0,0 +1,42 @@
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props.SummonerSpell do
defstruct [:id, :name, :win_rate, :wins, :total_games, :image]
@type t :: %{
id: integer(),
win_rate: float(),
wins: integer(),
total_games: integer(),
image: String.t(),
name: String.t()
}
end
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props do
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props.SummonerSpell
defstruct spell: %SummonerSpell{}
@type t :: %{spell: SummonerSpell.t()}
end
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpell do
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props
use Phoenix.Component
attr :spells, Props, default: %Props{}
def summoner_spells(assigns) do
~H"""
<div class="flex flex-wrap flex-wrap gap-4">
<%= for spell <- assigns.spells.summoner_spells do %>
<div clas="flex flex-col gap-1">
<img src={spell.spell.image} />
<p><%= spell.spell.name %></p>
<p><%= spell.spell.win_rate %>%</p>
<p><%= spell.spell.wins %>/<%= spell.spell.total_games %></p>
</div>
<% end %>
</div>
"""
end
end

View File

@ -1,36 +0,0 @@
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.SummonerSpell do
defstruct [:id, :win_rate, :total_games]
@type t :: %{
id: integer(),
win_rate: float(),
total_games: integer()
}
end
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props do
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.SummonerSpell
defstruct spell1: %SummonerSpell{},
spell2: %SummonerSpell{}
@type t :: %{
spell1: SummonerSpell.t(),
spell2: SummonerSpell.t()
}
end
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells do
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props
use Phoenix.Component
attr :spells, Props, default: %Props{}
def summoner_spells(assigns) do
~H"""
<div>
Spells
</div>
"""
end
end

View File

@ -302,6 +302,9 @@ defmodule LoLAnalyticsWeb.CoreComponents do
end
def input(%{type: "checkbox"} = assigns) do
IO.puts(">>>>")
IO.inspect(assigns)
assigns =
assign_new(assigns, :checked, fn ->
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
@ -315,8 +318,8 @@ defmodule LoLAnalyticsWeb.CoreComponents do
type="checkbox"
id={@id}
name={@name}
value="true"
checked={@checked}
value="false"
checked={false}
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
{@rest}
/>

View File

@ -2,25 +2,16 @@
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
<div class="flex items-center gap-4">
<a href="/">
<img src={~p"/images/logo.svg"} width="36" />
LoL Analytics
</a>
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
v<%= Application.spec(:phoenix, :vsn) %>
</p>
</div>
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
<a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
@elixirphoenix
</a>
<a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700">
GitHub
</a>
<a
href="https://hexdocs.pm/phoenix/overview.html"
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80"
>
Get Started <span aria-hidden="true">&rarr;</span>
</a>
<.link patch="/champions">
Champions
</.link>
</div>
</div>
</header>

View File

@ -1,17 +1,20 @@
<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title suffix=" · Phoenix Framework">
<%= assigns[:page_title] || "LoLAnalytics" %>
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="bg-slate-700 antialiased">
<%= @inner_content %>
</body>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title suffix=" · Phoenix Framework">
<%= assigns[:page_title] || "LoLAnalytics" %>
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="bg-white antialiased">
<%= @inner_content %>
</body>
</html>

View File

@ -0,0 +1,93 @@
defmodule LolAnalyticsWeb.ChampionLive.Components.ChampionFilters do
use LoLAnalyticsWeb, :live_component
@roles_definition [
%{title: "All", value: "all"},
%{title: "Top", value: "TOP"},
%{title: "Jungle", value: "JUNGLE"},
%{title: "Mid", value: "MIDDLE"},
%{title: "Bot", value: "BOTTOM"},
%{title: "Support", value: "UTILITY"}
]
def on_role_selected(role) do
send_update(LolAnalyticsWeb.ChampionLive, role)
end
def mount(socket) do
socket =
assign(socket, :roles, @roles_definition)
|> assign(:form, build_roles_form())
{:ok, socket}
end
defp build_roles() do
@roles_definition
|> Enum.reduce(%{}, fn role, acc ->
Map.merge(acc, %{"#{role.value}" => false})
end)
|> Map.merge(%{"all" => true})
end
defp build_roles_form() do
@roles_definition
|> Enum.reduce(%{}, fn role, acc ->
Map.merge(acc, %{"#{role.value}" => false})
end)
|> Map.merge(%{"all" => true})
|> to_form
end
def handle_event("filter", unsigned_params, socket) do
# updated_form =
# unsigned_params
# |> Enum.map(fn {key, val} -> {key, key == unsigned_params["_target"]} end)
IO.inspect(unsigned_params)
# # assign()
# {:noreply, socket |> assign(:form, to_form(updated_form))}
end
attr :selectedrole, :string, required: true
attr :roles, :list, default: []
def render(assigns) do
IO.puts(">>>>1")
# IO.inspect(assigns)
IO.inspect(assigns.selectedrole)
IO.inspect(">>>>>2")
selected_class =
"px-8 py-2 flex flex-row gap-2 align-middle rounded-md border-gray-200 border border-sky-500"
~H"""
<div>
<div class="flex flex-row justify-between p-10">
<%= for role <- @roles do %>
<%!-- <%= IO.inspect(role) %> --%>
<%= if (assigns.selectedrole == role.value) do %>
<div phx-click="filter" phx-value-role={role.title} class={selected_class}>
<%!-- <.input enabled={false} type="checkbox" field={@form[role.value]} /> --%>
<p><%= role.title %></p>
</div>
<% else %>
<div
phx-click="filter"
phx-value-role={role.value}
class="px-8 py-2 flex flex-row gap-2 align-middle"
>
<%!-- <.input enabled={false} type="checkbox" field={@form[role.value]} /> --%>
<p><%= role.title %></p>
</div>
<% end %>
<% end %>
</div>
</div>
"""
end
end
defmodule LolAnalyticsWeb.ChampionLive.Components.ChampionFilters.EventHandler do
@callback role_selected() :: none()
end

View File

@ -1,7 +1,13 @@
defmodule LoLAnalyticsWeb.ChampionLive.Index do
alias LolAnalyticsWeb.ChampionLive.Mapper
use LoLAnalyticsWeb, :live_view
import LolAnalyticsWeb.ChampionComponents.ChampionCard
alias LolAnalyticsWeb.ChampionLive.Mapper
alias LolAnalyticsWeb.ChampionLive.Components.ChampionFilters
@behaviour LolAnalyticsWeb.ChampionFilters.EventHandler
@roles [
%{title: "All", value: "all"},
%{title: "Top", value: "TOP"},
@ -20,77 +26,41 @@ defmodule LoLAnalyticsWeb.ChampionLive.Index do
|> Mapper.map_champs()
|> Enum.sort(&(&1.win_rate >= &2.win_rate))
roles =
@roles
|> Enum.reduce(%{}, fn role, acc ->
Map.merge(acc, %{"#{role.value}" => false})
end)
|> Map.merge(%{"all" => true})
form =
Map.merge(
%{"name" => ""},
roles
)
socket =
socket
|> stream(
:champions,
mapped
)
|> assign(:form, to_form(form))
|> assign(:roles, @roles)
|> stream(:champions, mapped)
|> assign(:selected_role, "all")
{:ok, socket}
end
@impl true
def handle_event("filter", params, socket) do
%{
"name" => query_name,
"all" => all,
"TOP" => top,
"JUNGLE" => jungle,
"MIDDLE" => mid,
"BOTTOM" => bot,
"UTILITY" => utility
} = params
filter =
if all == "true" do
nil
else
%{
"TOP" => top == "true",
"JUNGLE" => jungle == "true",
"MIDDLE" => mid == "true",
"BOTTOM" => bot == "true",
"UTILITY" => utility == "true"
}
|> Enum.filter(fn {_k, v} -> v end)
|> Enum.map(fn {k, _v} -> k end)
end
def handle_event("filter", %{"role" => selected_role} = params, socket) do
champs =
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates()
|> Enum.filter(fn %{name: name} ->
String.downcase(name) |> String.contains?(query_name)
end)
|> Enum.filter(fn champ ->
if filter != nil do
Enum.any?(filter, fn f -> f == champ.team_position end)
end
end)
|> Mapper.map_champs()
|> Enum.sort(&(&1.win_rate >= &2.win_rate))
|> filter_champs(selected_role)
{:noreply,
stream(
socket,
:champions,
champs
)}
socket
|> stream(:champions, champs)
|> assign(:selected_role, selected_role)}
end
def handle_event("filter_champs", params, socket) do
end
defp filter_champs(champs, selected_role) do
champs =
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates()
|> Enum.filter((&filter_role/1).(selected_role))
|> Mapper.map_champs()
|> Enum.sort(&(&1.win_rate >= &2.win_rate))
end
defp filter_role(role) do
fn champ ->
champ.team_position == role || role == "all"
end
end
@impl true
@ -101,7 +71,5 @@ defmodule LoLAnalyticsWeb.ChampionLive.Index do
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Champions")
# |> assign(:champion, nil)
end
end

View File

@ -1,54 +1,20 @@
<.header>
Listing Champions
Champions
</.header>
<h1>Champions</h1>
<.form for={@form} phx-change="filter" phx-submit="save">
<div class="flex flex-col gap-2">
<.input type="text" field={@form["name"]} />
<div class="flex flex-row justify-between">
<%= for role <- @roles do %>
<div class="flex flex-row gap-2 align-middle">
<.input type="checkbox" field={@form[role.value]} />
<p><%= role.title %></p>
</div>
<% end %>
</div>
</div>
<h1>Filters</h1>
<.live_component module={ChampionFilters} id="role-filters" selectedrole={@selected_role} />
<%!-- <button>Save</button> --%>
</.form>
<div id="champions" class="grid grid-cols-4 gap-4">
<%= for {_, champion} <- @streams.champions do %>
<div class="flex flex-col max-w-sm bg-white border border-gray-200 rounded-lg shadow hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700">
<img src={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{champion.image}"} />
<div class="flex-auto flex-col p-4 ">
<p><%= champion.name %></p>
<p><%= champion.wins %> / <%= champion.total_games %></p>
<p><%= champion.win_rate %>%</p>
<p><%= champion.team_position %></p>
</div>
</div>
<div class="sr-only">
<.link navigate={~p"/champions/#{champion}"}>Show</.link>
</div>
<% end %>
<.champion_card props={champion} />
<% end %>
</div>
<.modal
:if={@live_action in [:new, :edit]}
id="champion-modal"
show
on_cancel={JS.patch(~p"/champions")}
>
<.live_component
module={LoLAnalyticsWeb.ChampionLive.FormComponent}
id={@champion.champion_id || :new}
title={@page_title}
action={@live_action}
champion={@champion}
patch={~p"/champions"}
/>
<.modal :if={@live_action in [:new, :edit]} id="champion-modal" show on_cancel={JS.patch(~p"/champions")}>
<.live_component module={LoLAnalyticsWeb.ChampionLive.FormComponent} id={@champion.champion_id || :new}
title={@page_title} action={@live_action} champion={@champion} patch={~p"/champions"} />
</.modal>

View File

@ -5,7 +5,7 @@ defmodule LolAnalyticsWeb.ChampionLive.Mapper do
champs
|> Enum.map(fn champ ->
%{
Kernel.struct!(%ChampionSummary{}, champ)
champ
| win_rate: :erlang.float_to_binary(champ.win_rate, decimals: 2)
}
end)

View File

@ -1,7 +1,10 @@
defmodule LoLAnalyticsWeb.ChampionLive.Show do
use LoLAnalyticsWeb, :live_view
import LolAnalyticsWeb.ChampionComponents.SummonerSpells
import LolAnalyticsWeb.ChampionComponents.SummonerSpell
import LolAnalyticsWeb.ChampionComponents.ChampionAvatar
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.ShowMapper
@impl true
def mount(_params, _session, socket) do
@ -13,23 +16,22 @@ defmodule LoLAnalyticsWeb.ChampionLive.Show do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:champion, %{id: id})
|> assign(:summoner_spells, %{summoner_spells: load_summoner_spells()})}
|> assign(:champion, load_champion_info(id) |> ShowMapper.map_champion())
|> assign(:summoner_spells, %{
summoner_spells: load_summoner_spells(id) |> ShowMapper.map_spells()
})}
end
defp load_summoner_spells() do
%LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props{
spell1: %LolAnalyticsWeb.ChampionComponents.SummonerSpells.SummonerSpell{
id: 1,
win_rate: 51.7,
total_games: 400
},
spell2: %LolAnalyticsWeb.ChampionComponents.SummonerSpells.SummonerSpell{
id: 2,
win_rate: 51.7,
total_games: 500
}
}
defp load_summoner_spells(champion_id) do
LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo.get_champion_picked_summoners(champion_id)
end
defp load_champion_info(champion_id) do
champion = LolAnalytics.Dimensions.Champion.ChampionRepo.get_or_create(champion_id)
IO.inspect(champion)
champion
end
defp page_title(:show), do: "Show Champion"

View File

@ -1,9 +1,13 @@
<.header>
Champion <%= @champion.id %>
<:subtitle>This is a champion record from your database.</:subtitle>
<:actions></:actions>
<:actions></:actions>
</.header>
<.champion_avatar id={@champion.id} name={@champion.name}
image={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{@champion.image}"} />
<div class="my-4" />
<.summoner_spells spells={@summoner_spells} />
<.back navigate={~p"/champions"}>

View File

@ -0,0 +1,33 @@
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.ShowMapper do
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.SummonerSpell
@spec map_champion(%LolAnalytics.Dimensions.Champion.ChampionSchema{}) :: map()
def map_champion(champion) do
%{
id: champion.champion_id,
name: champion.name,
image: champion.image
}
end
def map_spells(items) do
items
|> Enum.map(&map_spell/1)
|> Enum.sort(&(&1.spell.total_games > &2.spell.total_games))
end
def map_spell(spell) do
image = spell.metadata["image"]["full"]
%LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props{
spell: %LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props.SummonerSpell{
id: spell.id,
win_rate: :erlang.float_to_binary(spell.win_rate, decimals: 2),
total_games: spell.total_games,
image: "https://ddragon.leagueoflegends.com/cdn/14.11.1/img/spell/#{image}",
name: spell.metadata["name"],
wins: spell.wins
}
}
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -0,0 +1,5 @@
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
# Disallow: /

Binary file not shown.

View File

@ -21,7 +21,7 @@
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},