Add Token Holdings box

pull/534/head
Felipe Renan 6 years ago
parent dc6bc8f51d
commit efe429e389
  1. 2
      apps/block_scout_web/assets/css/app.scss
  2. 35
      apps/block_scout_web/assets/css/components/_token-balance-dropdown.scss
  3. 1
      apps/block_scout_web/assets/js/app.js
  4. 19
      apps/block_scout_web/assets/js/lib/token_balance_dropdown.js
  5. 24
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_balance_controller.ex
  6. 7
      apps/block_scout_web/lib/block_scout_web/router.ex
  7. 20
      apps/block_scout_web/lib/block_scout_web/templates/address/_token_holdings.html.eex
  8. 5
      apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex
  9. 36
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex
  10. 17
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex
  11. 20
      apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex
  12. 30
      apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs

@ -76,7 +76,7 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "components/badge"; @import "components/badge";
@import "components/description-list"; @import "components/description-list";
@import "components/nounderline-link"; @import "components/nounderline-link";
@import "components/token-balance-dropdown";
:export { :export {
primary: $primary; primary: $primary;

@ -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;
}
}

@ -27,6 +27,7 @@ import './lib/tooltip'
import './lib/smart_contract/read_only_functions' import './lib/smart_contract/read_only_functions'
import './lib/pretty_json' import './lib/pretty_json'
import './lib/try_api' import './lib/try_api'
import './lib/token_balance_dropdown'
import './pages/address' import './pages/address'
import './pages/block' import './pages/block'

@ -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

@ -95,6 +95,13 @@ defmodule BlockScoutWeb.Router do
only: [:index, :show], only: [:index, :show],
as: :read_contract as: :read_contract
) )
resources(
"/token_balances",
AddressTokenBalanceController,
only: [:index],
as: :token_balance
)
end end
resources "/tokens", Tokens.TokenController, only: [:show], as: :token do resources "/tokens", Tokens.TokenController, only: [:show], as: :token do

@ -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>

@ -1,6 +1,6 @@
<section> <section>
<div class="row"> <div class="row">
<div class="col-md-12 col-lg-8"> <div class="col-md-12 col-lg-5">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="icon-links float-right"> <div class="icon-links float-right">
@ -54,6 +54,9 @@
<div class="col-md-6 col-lg-4" data-selector="balance-card"> <div class="col-md-6 col-lg-4" data-selector="balance-card">
<%= render BlockScoutWeb.AddressView, "_balance_card.html", address: @address, exchange_rate: @exchange_rate %> <%= render BlockScoutWeb.AddressView, "_balance_card.html", address: @address, exchange_rate: @exchange_rate %>
</div> </div>
<div class="col-md-6 col-lg-3">
<%= render BlockScoutWeb.AddressView, "_token_holdings.html", conn: @conn, address: @address %>
</div>
</div> </div>
</section> </section>

@ -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…
Cancel
Save