Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
159 lines
6.1 KiB
Elixir
159 lines
6.1 KiB
Elixir
defmodule TarakanWeb.BountyLive.Index do
|
|
@moduledoc "Open contracts - the public reward board."
|
|
use TarakanWeb, :live_view
|
|
|
|
import TarakanWeb.BountyComponents
|
|
|
|
alias Tarakan.Market
|
|
|
|
@filters [
|
|
{"all", "All"},
|
|
{"repository", "Repositories"},
|
|
{"infestation", "Infestations"},
|
|
{"finding", "Findings"}
|
|
]
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, "Contracts")
|
|
|> assign(
|
|
:meta_description,
|
|
"Open contracts on Tarakan. Sponsors post rewards for security work on public targets."
|
|
)
|
|
|> assign(:canonical_path, ~p"/bounties")
|
|
|> assign(:filters, @filters)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(params, _uri, socket) do
|
|
filter = normalize_filter(params["type"])
|
|
bounties = Market.list_open(target_type: filter)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:filter, filter || "all")
|
|
|> assign(:bounties, bounties)
|
|
|> assign(:bounty_count, length(bounties))
|
|
|> assign(:open_cents, sum_by(bounties, :amount_cents))
|
|
|> assign(:open_credits, sum_by(bounties, :credit_amount))}
|
|
end
|
|
|
|
defp sum_by(bounties, key) do
|
|
Enum.reduce(bounties, 0, fn bounty, total -> total + (Map.get(bounty, key) || 0) end)
|
|
end
|
|
|
|
defp normalize_filter(filter) when filter in ["repository", "infestation", "finding"],
|
|
do: filter
|
|
|
|
defp normalize_filter(_), do: nil
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<Layouts.app flash={@flash} current_scope={@current_scope} current_path={assigns[:current_path]}>
|
|
<Layouts.page width={:wide}>
|
|
<div class="flex flex-col gap-6 border-b-2 border-strong pb-6 sm:flex-row sm:items-end sm:justify-between">
|
|
<div class="min-w-0 max-w-2xl">
|
|
<h1 class="font-display text-3xl font-medium uppercase leading-none tracking-[0.02em] text-ink sm:text-5xl">
|
|
Contracts
|
|
</h1>
|
|
</div>
|
|
<div class="flex shrink-0 flex-wrap items-center gap-4">
|
|
<p class="font-mono text-sm tabular-nums text-ink">
|
|
<span class="font-display text-3xl">{@bounty_count}</span>
|
|
<span class="ml-1 text-xs uppercase tracking-[0.12em] text-ink-faint">open</span>
|
|
</p>
|
|
<p :if={@open_cents > 0} class="font-mono text-sm tabular-nums text-ink">
|
|
<span class="font-display text-3xl">${div(@open_cents, 100)}</span>
|
|
<span class="ml-1 text-xs uppercase tracking-[0.12em] text-ink-faint">escrowed</span>
|
|
</p>
|
|
<p :if={@open_credits > 0} class="font-mono text-sm tabular-nums text-ink">
|
|
<span class="font-display text-3xl">{@open_credits}</span>
|
|
<span class="ml-1 text-xs uppercase tracking-[0.12em] text-ink-faint">credits</span>
|
|
</p>
|
|
<.link
|
|
navigate={~p"/bounties/new"}
|
|
class="border-2 border-signal px-4 py-1.5 font-display text-xs uppercase tracking-[0.12em] text-signal transition hover:bg-signal hover:text-ground"
|
|
>
|
|
Post a contract
|
|
</.link>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="bounty-filters" class="mt-6 flex flex-wrap items-center gap-2">
|
|
<.link
|
|
:for={{value, label} <- @filters}
|
|
patch={~p"/bounties?#{%{type: value}}"}
|
|
class={[
|
|
"wire-badge font-display text-[10px] uppercase tracking-[0.12em] transition",
|
|
@filter == value && "bg-signal text-ground",
|
|
@filter != value && "text-ink-muted hover:text-ink"
|
|
]}
|
|
>
|
|
<span class="wire-badge-label">{label}</span>
|
|
</.link>
|
|
</div>
|
|
|
|
<div
|
|
:if={@bounties == []}
|
|
id="bounties-empty"
|
|
class="mt-8 bg-panel px-6 py-12 text-center"
|
|
>
|
|
<p class="text-sm font-medium text-ink">No open contracts right now</p>
|
|
<p class="mt-2 text-xs leading-5 text-ink-muted">
|
|
<.link navigate={~p"/bounties/new"} class="font-semibold text-signal hover:underline">
|
|
Post the first one
|
|
</.link>
|
|
or browse the <.link
|
|
navigate={~p"/jobs"}
|
|
class="font-semibold text-signal hover:underline"
|
|
>jobs queue</.link>.
|
|
</p>
|
|
</div>
|
|
|
|
<ul
|
|
:if={@bounties != []}
|
|
id="bounties"
|
|
class="mt-8 grid gap-4 sm:grid-cols-2 xl:grid-cols-3"
|
|
>
|
|
<li :for={bounty <- @bounties} id={"bounty-#{bounty.public_id}"}>
|
|
<.link
|
|
navigate={~p"/bounties/#{bounty.public_id}"}
|
|
class="group flex h-full flex-col border-2 border-strong bg-panel transition-colors hover:bg-ground"
|
|
>
|
|
<div class="flex items-center justify-between border-b-2 border-strong px-4 py-2">
|
|
<.notch_badge class="bg-signal text-ground">Contract</.notch_badge>
|
|
<span class="font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
|
|
{bounty.target_type}
|
|
</span>
|
|
</div>
|
|
<div class="flex flex-1 flex-col px-4 py-4">
|
|
<p class="font-display text-3xl font-medium leading-none text-signal">
|
|
{amount_label(bounty)}
|
|
</p>
|
|
<p class="mt-3 text-sm font-semibold leading-5 text-ink group-hover:text-signal">
|
|
{bounty.title}
|
|
</p>
|
|
<p class="mt-2 line-clamp-2 text-xs leading-5 text-ink-muted">
|
|
{bounty.description}
|
|
</p>
|
|
<p class="mt-auto pt-4 font-mono text-[11px] text-ink-faint">
|
|
{target_label(bounty)}
|
|
<span :if={bounty.sponsor_account} class="mx-1">·</span>
|
|
<span :if={bounty.sponsor_account}>by @{bounty.sponsor_account.handle}</span>
|
|
<span :if={bounty.expires_at} class="mx-1">·</span>
|
|
<span :if={bounty.expires_at}>
|
|
expires {Calendar.strftime(bounty.expires_at, "%b %-d")}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</.link>
|
|
</li>
|
|
</ul>
|
|
</Layouts.page>
|
|
</Layouts.app>
|
|
"""
|
|
end
|
|
end
|