Skip to content

Commit e6841ca

Browse files
authored
Merge pull request #2097 from hexlet-codebattle/improve-og-tags-for-t-and-g
Improve meta tags for links
2 parents b20e97c + 704f804 commit e6841ca

File tree

13 files changed

+448
-207
lines changed

13 files changed

+448
-207
lines changed

services/app/apps/codebattle/lib/codebattle/application.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ defmodule Codebattle.Application do
3939
children =
4040
[
4141
{ChromicPDF, chromic_pdf_opts()},
42+
{Codebattle.ImageCache, []},
4243
{Codebattle.Repo, []},
4344
{Registry, keys: :unique, name: Codebattle.Registry},
4445
CodebattleWeb.Telemetry,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
defmodule Codebattle.ImageCache do
2+
@moduledoc false
3+
use GenServer
4+
5+
# 2 hours in milliseconds
6+
@cleanup_interval 2 * 60 * 60 * 1000
7+
8+
def start_link(_opts) do
9+
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
10+
end
11+
12+
def init(:ok) do
13+
create_table()
14+
schedule_cleanup()
15+
{:ok, %{}}
16+
end
17+
18+
def create_table do
19+
:ets.new(
20+
:html_images,
21+
[
22+
:set,
23+
:public,
24+
:named_table,
25+
{:write_concurrency, true},
26+
{:read_concurrency, true}
27+
]
28+
)
29+
end
30+
31+
def put_image(cache_key, image) do
32+
:ets.insert(:html_images, {cache_key, image})
33+
:ok
34+
end
35+
36+
def get_image(cache_key) do
37+
case :ets.lookup(:html_images, cache_key) do
38+
[{^cache_key, cached_image}] ->
39+
cached_image
40+
41+
[] ->
42+
nil
43+
end
44+
end
45+
46+
def clean_table do
47+
:ets.delete_all_objects(:html_images)
48+
end
49+
50+
def handle_info(:cleanup, state) do
51+
clean_table()
52+
schedule_cleanup()
53+
{:noreply, state}
54+
end
55+
56+
defp schedule_cleanup do
57+
Process.send_after(self(), :cleanup, @cleanup_interval)
58+
end
59+
end
Lines changed: 129 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,159 @@
11
defmodule CodebattleWeb.Game.ImageController do
22
use CodebattleWeb, :controller
3+
use Gettext, backend: CodebattleWeb.Gettext
34

45
alias Codebattle.Game.Context
5-
6-
@fake_html_to_image Application.compile_env(:codebattle, :fake_html_to_image, false)
6+
alias Codebattle.Game.Player
7+
alias CodebattleWeb.HtmlImage
78

89
def show(conn, %{"game_id" => id}) do
910
case Context.fetch_game(id) do
1011
{:ok, game} ->
11-
{:ok, image} =
12-
game
13-
|> prepare_image_html()
14-
|> generate_png()
15-
16-
conn
17-
|> put_resp_content_type("image/png")
18-
|> send_resp(200, image)
12+
cache_key = "g_#{id}_#{game.state}"
13+
html = prepare_image_html(game)
14+
HtmlImage.render_image(conn, cache_key, html)
1915

20-
{:error, reason} ->
21-
conn
22-
|> put_status(:not_found)
23-
|> json(%{error: inspect(reason)})
16+
{:error, _reason} ->
17+
send_resp(conn, :ok, "")
2418
end
2519
end
2620

2721
defp prepare_image_html(game) do
2822
"""
29-
<html style="background-color:#dee2e6;">
30-
<center style="padding:25px;">
31-
<img src="https://codebattle.hexlet.io/assets/images/logo.svg" alt="Logo">
32-
<span>The Codebattle</span>
33-
#{render_content(game)}
34-
<p>Made with <span style="color: #e25555;">&#9829;</span> by CodebattleCoreTeam</p>
35-
<p>Dear frontenders, pls, make it prettier, thx</p>
36-
</center>
23+
<html>
24+
<head>
25+
<meta charset="utf-8">
26+
<style>
27+
html, body {
28+
margin: 0;
29+
padding: 0;
30+
width: 100%;
31+
height: 100%;
32+
background: #f5f7fa;
33+
font-family: 'Helvetica Neue', Arial, sans-serif;
34+
}
35+
p {
36+
margin: 0;
37+
padding: 0;
38+
}
39+
.card {
40+
width: 100%;
41+
height: 100%;
42+
background: #ffffff;
43+
border-radius: 8px;
44+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
45+
overflow: hidden;
46+
display: flex;
47+
flex-direction: column;
48+
}
49+
.header {
50+
background: #000;
51+
font-family: 'Helvetica Neue', Arial, sans-serif;
52+
color: #fff;
53+
text-align: center;
54+
}
55+
.header img {
56+
width: 100px;
57+
margin: 15px;
58+
height: auto;
59+
}
60+
.content {
61+
flex: 1;
62+
padding: 10px;
63+
color: #333;
64+
text-align: center;
65+
display: flex;
66+
flex-direction: column;
67+
justify-content: center;
68+
}
69+
.footer {
70+
text-align: center;
71+
font-size: 12px;
72+
color: #fff;
73+
padding: 8px;
74+
background: #000;
75+
}
76+
</style>
77+
</head>
78+
<body>
79+
<div class="card">
80+
<div class="header">
81+
<img src="#{HtmlImage.logo_url()}" alt="Logo">
82+
</div>
83+
<div class="content">
84+
#{render_content(game)}
85+
</div>
86+
<div class="footer">
87+
<p>
88+
Made with
89+
<svg width="14" height="14" viewBox="0 0 24 24" style="fill:#ff5252; vertical-align:middle;">
90+
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42
91+
4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81
92+
14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4
93+
6.86-8.55 11.54L12 21.35z"/>
94+
</svg>
95+
by Codebattle
96+
</p>
97+
</div>
98+
</div>
99+
</body>
37100
</html>
38101
"""
39102
end
40103

41-
defp render_content(%{players: []}) do
42-
"""
43-
<p>Codebattle game</p>
44-
"""
45-
end
104+
defp render_content(game) do
105+
level = Gettext.gettext(CodebattleWeb.Gettext, "Level: #{game.level}")
106+
state = Gettext.gettext(CodebattleWeb.Gettext, "Game state: #{game.state}")
46107

47-
defp render_content(%{players: [player1]} = game) do
48-
"""
49-
<p>Game state: #{game.state}</p>
50-
<p>Level: #{game.level}</p>
51-
#{render_player(player1)}
52-
"""
53-
end
108+
# If you always have exactly two players:
109+
[player1, player2] =
110+
case game.players do
111+
[p1, p2] -> [p1, p2]
112+
[p1] -> [p1, %Player{}]
113+
[] -> [%Player{}, %Player{}]
114+
end
54115

55-
defp render_content(%{players: [player1, player2]} = game) do
56116
"""
57-
<p>Game state: #{game.state}</p>
58-
<p>Level: #{game.level}</p>
59-
#{render_player(player1)}
60-
<span style="font-size:77px;">VS</span>
61-
#{render_player(player2)}
117+
<div style="display: flex; flex-direction: column; align-items: center; gap: 20px;">
118+
<div style="
119+
display: grid;
120+
grid-template-columns: 1fr auto 1fr;
121+
max-width: 800px;
122+
width: 100%;
123+
margin: 0 auto;
124+
align-items: center;
125+
text-align: center;
126+
">
127+
<!-- Player 1 -->
128+
<div style="justify-self: end;">
129+
#{render_player(player1)}
130+
</div>
131+
132+
<!-- VS -->
133+
<div style="justify-self: center; font-size: 42px; margin: 20px;">
134+
VS
135+
</div>
136+
137+
<!-- Player 2 -->
138+
<div style="justify-self: start;">
139+
#{render_player(player2)}
140+
</div>
141+
</div>
142+
<p>#{state}</p>
143+
<p>#{level}</p>
144+
</div>
62145
"""
63146
end
64147

65148
defp render_player(player) do
149+
result = Gettext.gettext(CodebattleWeb.Gettext, "#{player.result}")
150+
66151
"""
67-
<div style="display:inline-block">
68-
<center>
69-
<img src="#{player.avatar_url}" style="width:46px; height:46px">
70-
<p>@#{player.name} (#{player.lang}) - #{player.rating}</p>
71-
</center>
152+
<div>
153+
<img src="#{player.avatar_url || HtmlImage.logo_url()}" style="width:46px; height:46px;">
154+
<p>@#{player.name}(#{player.rating}) - #{player.lang}</p>
155+
<p>#{result}</p>
72156
</div>
73157
"""
74158
end
75-
76-
defp generate_png(html_content) do
77-
if @fake_html_to_image do
78-
{:ok, html_content}
79-
else
80-
ChromicPDF.capture_screenshot({:html, html_content}, capture_screenshot: %{format: "png"})
81-
end
82-
end
83159
end

0 commit comments

Comments
 (0)