Skip to content
Open
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
2 changes: 1 addition & 1 deletion lib/safira/accounts/roles/permissions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Safira.Accounts.Roles.Permissions do

def all do
%{
"attendees" => ["show", "edit"],
"attendees" => ["show", "edit", "show_leaderboard"],
"staffs" => ["show", "edit", "roles_edit"],
"challenges" => ["show", "edit", "delete"],
"companies" => ["edit"],
Expand Down
10 changes: 8 additions & 2 deletions lib/safira/inventory/item.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@ defmodule Safira.Inventory.Item do

@derive {
Flop.Schema,
filterable: [:product_name],
sortable: [:redeemed_at, :inserted_at],
filterable: [:product_name, :prize_name],
sortable: [:redeemed_at, :inserted_at, :product_name],
default_limit: 11,
join_fields: [
product_name: [
binding: :product,
field: :name,
path: [:product, :name],
ecto_type: :string
],
prize_name: [
binding: :prize,
field: :name,
path: [:prize, :name],
ecto_type: :string
]
]
}
Expand Down
19 changes: 19 additions & 0 deletions lib/safira/store.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,25 @@ defmodule Safira.Store do
|> Flop.validate_and_run(params, for: Item)
end

def list_items_type_prize(params) do
Item
|> join(:left, [i], p in assoc(i, :prize), as: :prize)
|> where([i], i.type == :prize and not is_nil(i.prize_id))
|> where([i], not is_nil(i.product_id))
|> preload(attendee: [:user], prize: [])
|> Flop.validate_and_run(params, for: Item)
end

def list_items_type_prize(%{} = params, opts) when is_list(opts) do
Item
|> join(:left, [i], p in assoc(i, :prize), as: :prize)
|> where([i], i.type == :prize)
|> where([i], not is_nil(i.product_id))
|> preload(attendee: [:user], prize: [])
|> apply_filters(opts)
|> Flop.validate_and_run(params, for: Item)
end

@doc """
Gets a single product.

Expand Down
13 changes: 13 additions & 0 deletions lib/safira_web/live/backoffice/attendee_live/index.html.heex
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
<.page title="Attendees">
<:actions>
<div class="flex flex-row w-full gap-4">
<.link patch={~p"/dashboard/attendees/leaderboard"}>
<.ensure_permissions
user={@current_user}
permissions={%{"attendees" => ["show_leaderboard"]}}
>
<.button>
<span class="flex flex-row items-center gap-2">
<.icon name="hero-trophy" class="w-5 h-5" />
<span class="hidden sm:block"><%= gettext("Leaderboard") %></span>
</span>
</.button>
</.ensure_permissions>
</.link>
<.link patch={~p"/downloads/attendees"} target="_blank">
<.button>
<span class="flex flex-row items-center gap-2">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule SafiraWeb.Backoffice.LeaderboardLive.Components.DaySelector do
@moduledoc """
Leaderboard component
"""

use SafiraWeb, :component

attr :day, :string, required: true
attr :on_left, :any, required: true
attr :on_right, :any, required: true
attr :left_enabled, :boolean, required: true
attr :right_enabled, :boolean, required: true

def day_selector(assigns) do
~H"""
<div class="flex justify-center">
<button disabled={not @left_enabled} class={enabled_class(@left_enabled)} phx-click={@on_left}>
<.icon name="hero-chevron-left" class="w-8 h-8" />
</button>
<h2 class="mx-8 text-4xl text-dark dark:text-light font-semibold uppercase"><%= @day %></h2>
<button
disabled={not @right_enabled}
class={enabled_class(@right_enabled)}
phx-click={@on_right}
>
<.icon name="hero-chevron-right" class="w-8 h-8" />
</button>
</div>
"""
end

defp enabled_class(enabled) do
if enabled do
""
else
"opacity-0"
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
defmodule SafiraWeb.Backoffice.LeaderboardLive.Components.Leaderboard do
@moduledoc """
Leaderboard component
"""

use SafiraWeb, :component

import SafiraWeb.Components.Avatar

attr :entries, :list, required: true

def leaderboard(assigns) do
~H"""
<div class="w-full">
<.leaderboard_top_3 entries={Enum.take(@entries, 3)} />
<ul class="flex flex-col gap-4 mt-6">
<.leaderboard_entry :for={entry <- Enum.drop(@entries, 3)} entry={entry} />
</ul>
</div>
"""
end

defp leaderboard_top_3(assigns) do
~H"""
<div class="flex flex-row justify-between w-full">
<.leaderboard_top_person entry={Enum.at(@entries, 1)} pos={2} />
<.leaderboard_top_person entry={Enum.at(@entries, 0)} winner={true} pos={1} />
<.leaderboard_top_person entry={Enum.at(@entries, 2)} pos={3} />
</div>
"""
end

attr :entry, :map, required: true
attr :winner, :boolean, default: false
attr :pos, :integer, required: true

defp leaderboard_top_person(assigns) do
~H"""
<%= if @entry do %>
<div class={["flex flex-col w-full items-center mt-8 mb-4", @winner && "-translate-y-20"]}>
<.icon
:if={@winner}
name="fa-crown fa-crown-solid"
class="w-10 h-10 translate-y-6 text-accent"
/>
<.avatar
handle={@entry.handle}
size={:xl}
class="bg-light/5 border-2 border-accent bg-accent rounded-full"
link={~p"/dashboard/attendees/#{@entry.attendee_id}"}
/>
<span class="bg-accent rounded-full px-2 -translate-y-3 select-none text-primary/80 font-semibold border-primary border-2">
<%= @pos %>
</span>
<p class="font-semibold truncate max-w-28 sm:max-w-full"><%= @entry.name %></p>
<p class="font-bold">
<%= gettext("%{badges_count} badges", badges_count: @entry.badges) %>
</p>
<p class="font-medium">
<%= gettext("%{tokens} tokens", tokens: @entry.tokens) %>
</p>
</div>
<% end %>
"""
end

attr :entry, :map, required: true

defp leaderboard_entry(assigns) do
~H"""
<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">
<div class="flex flex-row gap-4 items-center">
<p class="font-bold text-xl">
#<%= @entry.position %>
</p>
<p>
<.avatar
handle={@entry.handle}
size={:sm}
class="bg-light/5 border-2 border-light/5 rounded-full"
link={~p"/app/user/#{@entry.handle}"}
/>
</p>
<.link patch={~p"/dashboard/attendees/#{@entry.attendee_id}"}>
<p class="font-semibold truncate max-w-40">
<%= @entry.name %>
</p>
</.link>
</div>
<div>
<p class="font-semibold flex sm:flex-row flex-col items-center gap-2">
<span>
<%= @entry.badges %>
<span>
<%= gettext(" badges") %>
</span>
</span>
<span class="font-medium text-sm">
<%= @entry.tokens %>
<span>
<%= gettext(" tokens") %>
</span>
</span>
</p>
</div>
</li>
"""
end
end
126 changes: 126 additions & 0 deletions lib/safira_web/live/backoffice/attendee_live/leaderboard_live/index.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
defmodule SafiraWeb.Backoffice.AttendeeLive.LeaderboardLive.Index do
use SafiraWeb, :backoffice_view

alias Safira.{Contest, Event}
import SafiraWeb.Backoffice.LeaderboardLive.Components.{Leaderboard, DaySelector}

on_mount {SafiraWeb.StaffRoles, index: %{"attendees" => ["show_leaderboard"]}}

@limit 30

@impl true
def mount(_params, _session, socket) do
daily_prizes = Contest.list_daily_prizes()

days = Event.list_event_dates()

start_day_idx = get_start_day_idx(days)
leaderboard = Contest.leaderboard(Enum.at(days, start_day_idx), @limit)

{:ok,
socket
|> assign(:leaderboard, leaderboard)
|> assign(:current_page, :attendees)
|> assign(:current_day_str, display_current_day(days, start_day_idx))
|> assign(:current_day_index, start_day_idx)
|> assign(:days, days)
|> assign(:left_enabled, start_day_idx > 0)
|> assign(:right_enabled, start_day_idx < length(days) - 1)
|> assign(:daily_prizes, daily_prizes)
|> assign(:prizes, get_day_prizes(daily_prizes, Enum.at(days, start_day_idx)))}
end

@impl true
def handle_params(_params, _url, socket) do
{:noreply, socket}
end

@impl true
def handle_event("on_left", _, socket) do
if socket.assigns.current_day_index > 0 do
day = Enum.at(socket.assigns.days, socket.assigns.current_day_index - 1)

{:noreply,
socket
|> assign(:current_day_index, socket.assigns.current_day_index - 1)
|> assign(
:current_day_str,
display_current_day(socket.assigns.days, socket.assigns.current_day_index - 1)
)
|> assign(:left_enabled, socket.assigns.current_day_index > 1)
|> assign(:right_enabled, true)
|> assign(:prizes, get_day_prizes(socket.assigns.daily_prizes, day))
|> assign(:leaderboard, Contest.leaderboard(day, @limit))}
else
{:noreply, socket}
end
end

def handle_event("on_right", _, socket) do
if socket.assigns.current_day_index < length(socket.assigns.days) - 1 do
day = Enum.at(socket.assigns.days, socket.assigns.current_day_index + 1)

{:noreply,
socket
|> assign(:current_day_index, socket.assigns.current_day_index + 1)
|> assign(
:current_day_str,
display_current_day(socket.assigns.days, socket.assigns.current_day_index + 1)
)
|> assign(:left_enabled, true)
|> assign(
:right_enabled,
socket.assigns.current_day_index < length(socket.assigns.days) - 2
)
|> assign(:prizes, get_day_prizes(socket.assigns.daily_prizes, day))
|> assign(:leaderboard, Contest.leaderboard(day, @limit))}
else
{:noreply, socket}
end
end

defp get_start_day_idx(days) do
today = Date.utc_today()

idx = Enum.find_index(days, fn d -> d == today end)

if is_nil(idx) do
0
else
idx
end
end

defp display_current_day(days, index) do
days
|> Enum.at(index)
|> format_date()
end

defp format_date(date) do
today = Timex.today()
yesterday = Timex.shift(today, days: -1)

cond do
Timex.equal?(date, today) ->
gettext("Today")

Timex.equal?(date, yesterday) ->
gettext("Yesterday")

true ->
Timex.format!(date, "{D} {Mshort}")
end
end

defp get_day_prizes(daily_prizes, day) do
daily_prizes
|> Enum.filter(fn dp ->
if is_nil(day) do
is_nil(dp.date)
else
dp.date == day
end
end)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<.page title="Leaderboard">
<div class="flex flex-col gap-10 mt-6 max-w-3xl mx-auto">
<div>
<.day_selector
day={@current_day_str}
on_left="on_left"
on_right="on_right"
left_enabled={@left_enabled}
right_enabled={@right_enabled}
/>
</div>
<div class="md:mt-0 mt-8">
<.leaderboard :if={@leaderboard != []} entries={@leaderboard} />
<div :if={@leaderboard == []} class="flex flex-col items-center gap-4 my-20">
<img src={~p"/images/dizzy-void.svg"} class="w-32 h-32" />
<p><%= gettext("No data to show for this day.") %></p>
</div>
</div>
</div>
</.page>
1 change: 1 addition & 0 deletions lib/safira_web/live/backoffice/minigames_live/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule SafiraWeb.Backoffice.MinigamesLive.Index do

on_mount {SafiraWeb.StaffRoles,
index: %{"minigames" => ["show"]},
show: %{"minigames" => ["show"]},
simulate_wheel: %{"minigames" => ["simulate"]},
edit_wheel_drops: %{"minigames" => ["edit"]},
edit_wheel: %{"minigames" => ["edit"]},
Expand Down
10 changes: 10 additions & 0 deletions lib/safira_web/live/backoffice/minigames_live/index.html.heex
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
<.page title="Minigames">
<:actions>
<.link patch={~p"/dashboard/minigames/drops"}>
<.button>
<span class="flex flex-row items-center gap-2">
<.icon name="hero-trophy" class="w-5 h-5" />
<span class="hidden sm:block"><%= gettext("Drops") %></span>
</span>
</.button>
</.link>
</:actions>
<div class="grid sm:grid-cols-3 py-8 gap-5 sm:gap-10">
<.ensure_permissions user={@current_user} permissions={%{"minigames" => ["edit"]}}>
<.link
Expand Down
Loading