Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions services/app/apps/codebattle/lib/codebattle/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ defmodule Codebattle.Application do
children =
[
{ChromicPDF, chromic_pdf_opts()},
{Codebattle.ImageCache, []},
{Codebattle.Repo, []},
{Registry, keys: :unique, name: Codebattle.Registry},
CodebattleWeb.Telemetry,
Expand Down
59 changes: 59 additions & 0 deletions services/app/apps/codebattle/lib/codebattle/image_cache.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
defmodule Codebattle.ImageCache do
@moduledoc false
use GenServer

# 2 hours in milliseconds
@cleanup_interval 2 * 60 * 60 * 1000

def start_link(_opts) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end

def init(:ok) do
create_table()
schedule_cleanup()
{:ok, %{}}
end

def create_table do
:ets.new(
:html_images,
[
:set,
:public,
:named_table,
{:write_concurrency, true},
{:read_concurrency, true}
]
)
end

def put_image(cache_key, image) do
:ets.insert(:html_images, {cache_key, image})
:ok
end

def get_image(cache_key) do
case :ets.lookup(:html_images, cache_key) do
[{^cache_key, cached_image}] ->
cached_image

[] ->
nil
end
end

def clean_table do
:ets.delete_all_objects(:html_images)
end

def handle_info(:cleanup, state) do
clean_table()
schedule_cleanup()
{:noreply, state}
end

defp schedule_cleanup do
Process.send_after(self(), :cleanup, @cleanup_interval)
end
end
Original file line number Diff line number Diff line change
@@ -1,83 +1,159 @@
defmodule CodebattleWeb.Game.ImageController do
use CodebattleWeb, :controller
use Gettext, backend: CodebattleWeb.Gettext

alias Codebattle.Game.Context

@fake_html_to_image Application.compile_env(:codebattle, :fake_html_to_image, false)
alias Codebattle.Game.Player
alias CodebattleWeb.HtmlImage

def show(conn, %{"game_id" => id}) do
case Context.fetch_game(id) do
{:ok, game} ->
{:ok, image} =
game
|> prepare_image_html()
|> generate_png()

conn
|> put_resp_content_type("image/png")
|> send_resp(200, image)
cache_key = "g_#{id}_#{game.state}"
html = prepare_image_html(game)
HtmlImage.render_image(conn, cache_key, html)

{:error, reason} ->
conn
|> put_status(:not_found)
|> json(%{error: inspect(reason)})
{:error, _reason} ->
send_resp(conn, :ok, "")
end
end

defp prepare_image_html(game) do
"""
<html style="background-color:#dee2e6;">
<center style="padding:25px;">
<img src="https://codebattle.hexlet.io/assets/images/logo.svg" alt="Logo">
<span>The Codebattle</span>
#{render_content(game)}
<p>Made with <span style="color: #e25555;">&#9829;</span> by CodebattleCoreTeam</p>
<p>Dear frontenders, pls, make it prettier, thx</p>
</center>
<html>
<head>
<meta charset="utf-8">
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: #f5f7fa;
font-family: 'Helvetica Neue', Arial, sans-serif;
}
p {
margin: 0;
padding: 0;
}
.card {
width: 100%;
height: 100%;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
overflow: hidden;
display: flex;
flex-direction: column;
}
.header {
background: #000;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #fff;
text-align: center;
}
.header img {
width: 100px;
margin: 15px;
height: auto;
}
.content {
flex: 1;
padding: 10px;
color: #333;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
}
.footer {
text-align: center;
font-size: 12px;
color: #fff;
padding: 8px;
background: #000;
}
</style>
</head>
<body>
<div class="card">
<div class="header">
<img src="#{HtmlImage.logo_url()}" alt="Logo">
</div>
<div class="content">
#{render_content(game)}
</div>
<div class="footer">
<p>
Made with
<svg width="14" height="14" viewBox="0 0 24 24" style="fill:#ff5252; vertical-align:middle;">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42
4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81
14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4
6.86-8.55 11.54L12 21.35z"/>
</svg>
by Codebattle
</p>
</div>
</div>
</body>
</html>
"""
end

defp render_content(%{players: []}) do
"""
<p>Codebattle game</p>
"""
end
defp render_content(game) do
level = Gettext.gettext(CodebattleWeb.Gettext, "Level: #{game.level}")
state = Gettext.gettext(CodebattleWeb.Gettext, "Game state: #{game.state}")

defp render_content(%{players: [player1]} = game) do
"""
<p>Game state: #{game.state}</p>
<p>Level: #{game.level}</p>
#{render_player(player1)}
"""
end
# If you always have exactly two players:
[player1, player2] =
case game.players do
[p1, p2] -> [p1, p2]
[p1] -> [p1, %Player{}]
[] -> [%Player{}, %Player{}]
end

defp render_content(%{players: [player1, player2]} = game) do
"""
<p>Game state: #{game.state}</p>
<p>Level: #{game.level}</p>
#{render_player(player1)}
<span style="font-size:77px;">VS</span>
#{render_player(player2)}
<div style="display: flex; flex-direction: column; align-items: center; gap: 20px;">
<div style="
display: grid;
grid-template-columns: 1fr auto 1fr;
max-width: 800px;
width: 100%;
margin: 0 auto;
align-items: center;
text-align: center;
">
<!-- Player 1 -->
<div style="justify-self: end;">
#{render_player(player1)}
</div>

<!-- VS -->
<div style="justify-self: center; font-size: 42px; margin: 20px;">
VS
</div>

<!-- Player 2 -->
<div style="justify-self: start;">
#{render_player(player2)}
</div>
</div>
<p>#{state}</p>
<p>#{level}</p>
</div>
"""
end

defp render_player(player) do
result = Gettext.gettext(CodebattleWeb.Gettext, "#{player.result}")

"""
<div style="display:inline-block">
<center>
<img src="#{player.avatar_url}" style="width:46px; height:46px">
<p>@#{player.name} (#{player.lang}) - #{player.rating}</p>
</center>
<div>
<img src="#{player.avatar_url || HtmlImage.logo_url()}" style="width:46px; height:46px;">
<p>@#{player.name}(#{player.rating}) - #{player.lang}</p>
<p>#{result}</p>
</div>
"""
end

defp generate_png(html_content) do
if @fake_html_to_image do
{:ok, html_content}
else
ChromicPDF.capture_screenshot({:html, html_content}, capture_screenshot: %{format: "png"})
end
end
end
Loading
Loading