Skip to content

Commit bc4b5eb

Browse files
authored
feat: backoffice leaderboard page (#528)
1 parent 6872103 commit bc4b5eb

File tree

7 files changed

+309
-1
lines changed

7 files changed

+309
-1
lines changed

lib/safira/accounts/roles/permissions.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule Safira.Accounts.Roles.Permissions do
66

77
def all do
88
%{
9-
"attendees" => ["show", "edit"],
9+
"attendees" => ["show", "edit", "show_leaderboard"],
1010
"staffs" => ["show", "edit", "roles_edit"],
1111
"challenges" => ["show", "edit", "delete"],
1212
"companies" => ["edit"],

lib/safira_web/live/backoffice/attendee_live/index.html.heex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
<.page title="Attendees">
22
<:actions>
33
<div class="flex flex-row w-full gap-4">
4+
<.link patch={~p"/dashboard/attendees/leaderboard"}>
5+
<.ensure_permissions
6+
user={@current_user}
7+
permissions={%{"attendees" => ["show_leaderboard"]}}
8+
>
9+
<.button>
10+
<span class="flex flex-row items-center gap-2">
11+
<.icon name="hero-trophy" class="w-5 h-5" />
12+
<span class="hidden sm:block"><%= gettext("Leaderboard") %></span>
13+
</span>
14+
</.button>
15+
</.ensure_permissions>
16+
</.link>
417
<.link patch={~p"/downloads/attendees"} target="_blank">
518
<.button>
619
<span class="flex flex-row items-center gap-2">
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
defmodule SafiraWeb.Backoffice.LeaderboardLive.Components.DaySelector do
2+
@moduledoc """
3+
Leaderboard component
4+
"""
5+
6+
use SafiraWeb, :component
7+
8+
attr :day, :string, required: true
9+
attr :on_left, :any, required: true
10+
attr :on_right, :any, required: true
11+
attr :left_enabled, :boolean, required: true
12+
attr :right_enabled, :boolean, required: true
13+
14+
def day_selector(assigns) do
15+
~H"""
16+
<div class="flex justify-center">
17+
<button disabled={not @left_enabled} class={enabled_class(@left_enabled)} phx-click={@on_left}>
18+
<.icon name="hero-chevron-left" class="w-8 h-8" />
19+
</button>
20+
<h2 class="mx-8 text-4xl text-dark dark:text-light font-semibold uppercase"><%= @day %></h2>
21+
<button
22+
disabled={not @right_enabled}
23+
class={enabled_class(@right_enabled)}
24+
phx-click={@on_right}
25+
>
26+
<.icon name="hero-chevron-right" class="w-8 h-8" />
27+
</button>
28+
</div>
29+
"""
30+
end
31+
32+
defp enabled_class(enabled) do
33+
if enabled do
34+
""
35+
else
36+
"opacity-0"
37+
end
38+
end
39+
end
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
defmodule SafiraWeb.Backoffice.LeaderboardLive.Components.Leaderboard do
2+
@moduledoc """
3+
Leaderboard component
4+
"""
5+
6+
use SafiraWeb, :component
7+
8+
import SafiraWeb.Components.Avatar
9+
10+
attr :entries, :list, required: true
11+
12+
def leaderboard(assigns) do
13+
~H"""
14+
<div class="w-full">
15+
<.leaderboard_top_3 entries={Enum.take(@entries, 3)} />
16+
<ul class="flex flex-col gap-4 mt-6">
17+
<.leaderboard_entry :for={entry <- Enum.drop(@entries, 3)} entry={entry} />
18+
</ul>
19+
</div>
20+
"""
21+
end
22+
23+
defp leaderboard_top_3(assigns) do
24+
~H"""
25+
<div class="flex flex-row justify-between w-full">
26+
<.leaderboard_top_person entry={Enum.at(@entries, 1)} pos={2} />
27+
<.leaderboard_top_person entry={Enum.at(@entries, 0)} winner={true} pos={1} />
28+
<.leaderboard_top_person entry={Enum.at(@entries, 2)} pos={3} />
29+
</div>
30+
"""
31+
end
32+
33+
attr :entry, :map, required: true
34+
attr :winner, :boolean, default: false
35+
attr :pos, :integer, required: true
36+
37+
defp leaderboard_top_person(assigns) do
38+
~H"""
39+
<%= if @entry do %>
40+
<div class={["flex flex-col w-full items-center mt-8 mb-4", @winner && "-translate-y-20"]}>
41+
<.icon
42+
:if={@winner}
43+
name="fa-crown fa-crown-solid"
44+
class="w-10 h-10 translate-y-6 text-accent"
45+
/>
46+
<.avatar
47+
handle={@entry.handle}
48+
size={:xl}
49+
class="bg-light/5 border-2 border-accent bg-accent rounded-full"
50+
link={~p"/dashboard/attendees/#{@entry.attendee_id}"}
51+
/>
52+
<span class="bg-accent rounded-full px-2 -translate-y-3 select-none text-primary/80 font-semibold border-primary border-2">
53+
<%= @pos %>
54+
</span>
55+
<p class="font-semibold truncate max-w-28 sm:max-w-full"><%= @entry.name %></p>
56+
<p class="font-bold">
57+
<%= gettext("%{badges_count} badges", badges_count: @entry.badges) %>
58+
</p>
59+
<p class="font-medium">
60+
<%= gettext("%{tokens} tokens", tokens: @entry.tokens) %>
61+
</p>
62+
</div>
63+
<% end %>
64+
"""
65+
end
66+
67+
attr :entry, :map, required: true
68+
69+
defp leaderboard_entry(assigns) do
70+
~H"""
71+
<li class="flex flex-row py-3 px-4 rounded-lg justify-between items-center bg-darkMuted/5 dark:bg-light/5 text-dark dark:text-light">
72+
<div class="flex flex-row gap-4 items-center">
73+
<p class="font-bold text-xl">
74+
#<%= @entry.position %>
75+
</p>
76+
<p>
77+
<.avatar
78+
handle={@entry.handle}
79+
size={:sm}
80+
class="bg-light/5 border-2 border-light/5 rounded-full"
81+
link={~p"/app/user/#{@entry.handle}"}
82+
/>
83+
</p>
84+
<.link patch={~p"/dashboard/attendees/#{@entry.attendee_id}"}>
85+
<p class="font-semibold truncate max-w-40">
86+
<%= @entry.name %>
87+
</p>
88+
</.link>
89+
</div>
90+
<div>
91+
<p class="font-semibold flex sm:flex-row flex-col items-center gap-2">
92+
<span>
93+
<%= @entry.badges %>
94+
<span>
95+
<%= gettext(" badges") %>
96+
</span>
97+
</span>
98+
<span class="font-medium text-sm">
99+
<%= @entry.tokens %>
100+
<span>
101+
<%= gettext(" tokens") %>
102+
</span>
103+
</span>
104+
</p>
105+
</div>
106+
</li>
107+
"""
108+
end
109+
end
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
defmodule SafiraWeb.Backoffice.AttendeeLive.LeaderboardLive.Index do
2+
use SafiraWeb, :backoffice_view
3+
4+
alias Safira.{Contest, Event}
5+
import SafiraWeb.Backoffice.LeaderboardLive.Components.{Leaderboard, DaySelector}
6+
7+
on_mount {SafiraWeb.StaffRoles, index: %{"attendees" => ["show_leaderboard"]}}
8+
9+
@limit 30
10+
11+
@impl true
12+
def mount(_params, _session, socket) do
13+
daily_prizes = Contest.list_daily_prizes()
14+
15+
days = Event.list_event_dates()
16+
17+
start_day_idx = get_start_day_idx(days)
18+
leaderboard = Contest.leaderboard(Enum.at(days, start_day_idx), @limit)
19+
20+
{:ok,
21+
socket
22+
|> assign(:leaderboard, leaderboard)
23+
|> assign(:current_page, :attendees)
24+
|> assign(:current_day_str, display_current_day(days, start_day_idx))
25+
|> assign(:current_day_index, start_day_idx)
26+
|> assign(:days, days)
27+
|> assign(:left_enabled, start_day_idx > 0)
28+
|> assign(:right_enabled, start_day_idx < length(days) - 1)
29+
|> assign(:daily_prizes, daily_prizes)
30+
|> assign(:prizes, get_day_prizes(daily_prizes, Enum.at(days, start_day_idx)))}
31+
end
32+
33+
@impl true
34+
def handle_params(_params, _url, socket) do
35+
{:noreply, socket}
36+
end
37+
38+
@impl true
39+
def handle_event("on_left", _, socket) do
40+
if socket.assigns.current_day_index > 0 do
41+
day = Enum.at(socket.assigns.days, socket.assigns.current_day_index - 1)
42+
43+
{:noreply,
44+
socket
45+
|> assign(:current_day_index, socket.assigns.current_day_index - 1)
46+
|> assign(
47+
:current_day_str,
48+
display_current_day(socket.assigns.days, socket.assigns.current_day_index - 1)
49+
)
50+
|> assign(:left_enabled, socket.assigns.current_day_index > 1)
51+
|> assign(:right_enabled, true)
52+
|> assign(:prizes, get_day_prizes(socket.assigns.daily_prizes, day))
53+
|> assign(:leaderboard, Contest.leaderboard(day, @limit))}
54+
else
55+
{:noreply, socket}
56+
end
57+
end
58+
59+
def handle_event("on_right", _, socket) do
60+
if socket.assigns.current_day_index < length(socket.assigns.days) - 1 do
61+
day = Enum.at(socket.assigns.days, socket.assigns.current_day_index + 1)
62+
63+
{:noreply,
64+
socket
65+
|> assign(:current_day_index, socket.assigns.current_day_index + 1)
66+
|> assign(
67+
:current_day_str,
68+
display_current_day(socket.assigns.days, socket.assigns.current_day_index + 1)
69+
)
70+
|> assign(:left_enabled, true)
71+
|> assign(
72+
:right_enabled,
73+
socket.assigns.current_day_index < length(socket.assigns.days) - 2
74+
)
75+
|> assign(:prizes, get_day_prizes(socket.assigns.daily_prizes, day))
76+
|> assign(:leaderboard, Contest.leaderboard(day, @limit))}
77+
else
78+
{:noreply, socket}
79+
end
80+
end
81+
82+
defp get_start_day_idx(days) do
83+
today = Date.utc_today()
84+
85+
idx = Enum.find_index(days, fn d -> d == today end)
86+
87+
if is_nil(idx) do
88+
0
89+
else
90+
idx
91+
end
92+
end
93+
94+
defp display_current_day(days, index) do
95+
days
96+
|> Enum.at(index)
97+
|> format_date()
98+
end
99+
100+
defp format_date(date) do
101+
today = Timex.today()
102+
yesterday = Timex.shift(today, days: -1)
103+
104+
cond do
105+
Timex.equal?(date, today) ->
106+
gettext("Today")
107+
108+
Timex.equal?(date, yesterday) ->
109+
gettext("Yesterday")
110+
111+
true ->
112+
Timex.format!(date, "{D} {Mshort}")
113+
end
114+
end
115+
116+
defp get_day_prizes(daily_prizes, day) do
117+
daily_prizes
118+
|> Enum.filter(fn dp ->
119+
if is_nil(day) do
120+
is_nil(dp.date)
121+
else
122+
dp.date == day
123+
end
124+
end)
125+
end
126+
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<.page title="Leaderboard">
2+
<div class="flex flex-col gap-10 mt-6 max-w-3xl mx-auto">
3+
<div>
4+
<.day_selector
5+
day={@current_day_str}
6+
on_left="on_left"
7+
on_right="on_right"
8+
left_enabled={@left_enabled}
9+
right_enabled={@right_enabled}
10+
/>
11+
</div>
12+
<div class="md:mt-0 mt-8">
13+
<.leaderboard :if={@leaderboard != []} entries={@leaderboard} />
14+
<div :if={@leaderboard == []} class="flex flex-col items-center gap-4 my-20">
15+
<img src={~p"/images/dizzy-void.svg"} class="w-32 h-32" />
16+
<p><%= gettext("No data to show for this day.") %></p>
17+
</div>
18+
</div>
19+
</div>
20+
</.page>

lib/safira_web/router.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ defmodule SafiraWeb.Router do
171171
end
172172

173173
scope "/attendees", AttendeeLive do
174+
live "/leaderboard", LeaderboardLive.Index, :index
174175
live "/", Index, :index
175176
live "/:id", Show, :show
176177
live "/:id/edit/tokens", Show, :tokens_edit

0 commit comments

Comments
 (0)