parent
dc6bc8f51d
commit
efe429e389
@ -0,0 +1,35 @@ |
||||
.token-balance-dropdown { |
||||
min-width: 14.375rem; |
||||
margin-top: 1rem; |
||||
background-color: $gray-100; |
||||
box-shadow: 0px 2px 3px 2px $gray-200; |
||||
border: none; |
||||
|
||||
// Overriding style added by Bootstrap dropdown via JS. |
||||
left: -17px !important; |
||||
|
||||
.dropdown-items { |
||||
overflow-y: auto; |
||||
max-height: 18.5rem; |
||||
|
||||
.dropdown-item:hover { |
||||
color: $white; |
||||
} |
||||
} |
||||
|
||||
&:after, &:before { |
||||
bottom: 100%; |
||||
left: 14%; |
||||
border: solid transparent; |
||||
content: " "; |
||||
height: 0; |
||||
width: 0; |
||||
position: absolute; |
||||
} |
||||
|
||||
&:before { |
||||
border-bottom-color: $gray-100; |
||||
border-width: 1rem; |
||||
margin-left: -1rem; |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
import $ from 'jquery' |
||||
|
||||
const tokenBalanceDropdown = (element) => { |
||||
const $element = $(element) |
||||
const $loading = $element.find('[data-loading]') |
||||
const $errorMessage = $element.find('[data-error-message]') |
||||
const apiPath = element.dataset.api_path |
||||
|
||||
$loading.show() |
||||
|
||||
$.get(apiPath) |
||||
.done(response => $element.html(response)) |
||||
.fail(() => { |
||||
$loading.hide() |
||||
$errorMessage.show() |
||||
}) |
||||
} |
||||
|
||||
$('[data-token-balance-dropdown]').each((_index, element) => tokenBalanceDropdown(element)) |
@ -0,0 +1,24 @@ |
||||
defmodule BlockScoutWeb.AddressTokenBalanceController do |
||||
use BlockScoutWeb, :controller |
||||
|
||||
alias Explorer.Chain |
||||
alias Explorer.Token.BalanceReader |
||||
|
||||
def index(conn, %{"address_id" => address_hash_string}) do |
||||
with true <- ajax?(conn), |
||||
{:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string) do |
||||
token_balances = |
||||
address_hash |
||||
|> Chain.fetch_tokens_from_address_hash() |
||||
|> BalanceReader.fetch_token_balances_without_error(address_hash_string) |
||||
|
||||
conn |
||||
|> put_status(200) |
||||
|> put_layout(false) |
||||
|> render("_token_balances.html", tokens: token_balances) |
||||
else |
||||
_ -> |
||||
not_found(conn) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,20 @@ |
||||
<div class="card"> |
||||
<div class="card-body"> |
||||
<h2 class="card-title"><%= gettext "Token Holdings" %></h2> |
||||
|
||||
<!-- Dropdown --> |
||||
<div data-token-balance-dropdown |
||||
data-api_path=<%= address_token_balance_path(@conn, :index, :en, @address.hash) %> |
||||
class="icon-links ml-3 mb-3" |
||||
> |
||||
<p data-loading class="mb-0" stytle="display: none"> |
||||
<i class="fa fa-spinner fa-spin"></i> |
||||
<%= gettext("Fetching tokens...") %> |
||||
</p> |
||||
|
||||
<p data-error-message class="mb-0" style="display: none"> |
||||
<%= gettext("Error tryng to fetch balances.") %> |
||||
</p> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -0,0 +1,36 @@ |
||||
<%= if Enum.any?(@tokens) do %> |
||||
<a href="#" |
||||
data-dropdown-toggle |
||||
data-toggle="dropdown" |
||||
role="button" |
||||
class="icon-link" |
||||
id="dropdown-tokens" |
||||
aria-haspopup="true" |
||||
aria-expanded="false" |
||||
style="text-decoration: none;"> |
||||
<i class="fas fa-chevron-circle-down"></i> |
||||
</a> |
||||
<% end %> |
||||
<h4 data-tokens-count class="ml-2 text-dark"><%= tokens_count_title(@tokens) %></h4> |
||||
|
||||
<div class="dropdown-menu p-0 token-balance-dropdown" aria-labelledby="dropdown-tokens"> |
||||
<div data-dropdown-items class="dropdown-items"> |
||||
<%= if Enum.any?(@tokens, & &1.type == "ERC-721") do %> |
||||
<%= render( |
||||
"_tokens.html", |
||||
conn: @conn, |
||||
tokens: filter_by_type(@tokens, "ERC-721"), |
||||
type: "ERC-721" |
||||
) %> |
||||
<% end %> |
||||
|
||||
<%= if Enum.any?(@tokens, & &1.type == "ERC-20") do %> |
||||
<%= render( |
||||
"_tokens.html", |
||||
conn: @conn, |
||||
tokens: filter_by_type(@tokens, "ERC-20"), |
||||
type: "ERC-20" |
||||
) %> |
||||
<% end %> |
||||
</div> |
||||
</div> |
@ -0,0 +1,17 @@ |
||||
<h6 class="dropdown-header border-bottom"> |
||||
<%= @type %> (<%= Enum.count(@tokens)%>) |
||||
</h6> |
||||
|
||||
<%= for token <- sort_by_name(@tokens) do %> |
||||
<div class="border-bottom"> |
||||
<%= link( |
||||
to: token_path(@conn, :show, :en, token.contract_address_hash), |
||||
class: "dropdown-item" |
||||
) do %> |
||||
<p class="mb-0"><%= token_name(token) %></p> |
||||
<p class="mb-0"> |
||||
<%= format_according_to_decimals(token.balance, token.decimals) %> <%= token.symbol %> |
||||
</p> |
||||
<% end %> |
||||
</div> |
||||
<% end %> |
@ -0,0 +1,20 @@ |
||||
defmodule BlockScoutWeb.AddressTokenBalanceView do |
||||
use BlockScoutWeb, :view |
||||
|
||||
def tokens_count_title(tokens) do |
||||
ngettext("%{count} token", "%{count} tokens", Enum.count(tokens)) |
||||
end |
||||
|
||||
def filter_by_type(tokens, type) do |
||||
Enum.filter(tokens, &(&1.type == type)) |
||||
end |
||||
|
||||
@doc """ |
||||
Sorts the given list of tokens in alphabetically order considering nil values in the bottom of |
||||
the list. |
||||
""" |
||||
def sort_by_name(tokens) do |
||||
{unnamed, named} = Enum.split_with(tokens, &is_nil(&1.name)) |
||||
Enum.sort_by(named, &String.downcase(&1.name)) ++ unnamed |
||||
end |
||||
end |
@ -0,0 +1,30 @@ |
||||
defmodule BlockScoutWeb.AddressTokenBalanceViewTest do |
||||
use BlockScoutWeb.ConnCase, async: true |
||||
|
||||
alias BlockScoutWeb.AddressTokenBalanceView |
||||
|
||||
describe "sort_by_name/1" do |
||||
test "sorts the given tokens by its name" do |
||||
token_a = build(:token, name: "token name") |
||||
token_b = build(:token, name: "token") |
||||
token_c = build(:token, name: "atoken") |
||||
|
||||
assert AddressTokenBalanceView.sort_by_name([token_a, token_b, token_c]) == [token_c, token_b, token_a] |
||||
end |
||||
|
||||
test "considers nil values in the bottom of the list" do |
||||
token_a = build(:token, name: nil) |
||||
token_b = build(:token, name: "token name") |
||||
token_c = build(:token, name: "token") |
||||
|
||||
assert AddressTokenBalanceView.sort_by_name([token_a, token_b, token_c]) == [token_c, token_b, token_a] |
||||
end |
||||
|
||||
test "considers capitalization" do |
||||
token_a = build(:token, name: "Token") |
||||
token_b = build(:token, name: "atoken") |
||||
|
||||
assert AddressTokenBalanceView.sort_by_name([token_a, token_b]) == [token_b, token_a] |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue