Skip to content

Commit

Permalink
feat(portfolio): implement TokensCard component (#6157)
Browse files Browse the repository at this point in the history
# Motivation

The `TokensCard` component on the Portfolio page displays the total USD
amount of tokens and a list of up to four tokens. If there are fewer
than three tokens, it shows a section with additional information.

This component will receive data from the Portfolio page. The page will
take the user's token list and convert it into a list of four relevant
tokens because other components on the page also require this
information.

A future PR will enhance the styling of the subcomponents.

Related PRs:
- Get top tokens #6153

# Changes

- Adds the `TokensCard` component.

# Tests

- Unit tests for the component  
-  New page object for the component 
- Manually tested in [E2E
environment](https://qsgjb-riaaa-aaaaa-aaaga-cai.yhabib-ingress.devenv.dfinity.network/)

# Todos

- [ ] Add entry to changelog (if necessary).
Not necessary
  • Loading branch information
yhabib authored Jan 14, 2025
1 parent da5810d commit 90d8729
Show file tree
Hide file tree
Showing 6 changed files with 564 additions and 1 deletion.
283 changes: 283 additions & 0 deletions frontend/src/lib/components/portfolio/TokensCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
<script lang="ts">
import AmountDisplay from "$lib/components/ic/AmountDisplay.svelte";
import Card from "$lib/components/portfolio/Card.svelte";
import { PRICE_NOT_AVAILABLE_PLACEHOLDER } from "$lib/constants/constants";
import { AppPath } from "$lib/constants/routes.constants";
import { authSignedInStore } from "$lib/derived/auth.derived";
import { i18n } from "$lib/stores/i18n";
import type { UserTokenData } from "$lib/types/tokens-page";
import { formatNumber } from "$lib/utils/format.utils";
import { IconAccountsPage, IconRight } from "@dfinity/gix-components";
import Logo from "../ui/Logo.svelte";
export let topTokens: UserTokenData[];
export let usdAmount: number;
const href = AppPath.Tokens;
let usdAmountFormatted: string;
$: usdAmountFormatted = $authSignedInStore
? formatNumber(usdAmount)
: PRICE_NOT_AVAILABLE_PLACEHOLDER;
// TODO: This will also depend on the number of projects
let showInfoRow: boolean;
$: showInfoRow = topTokens.length > 0 && topTokens.length < 3;
</script>

<Card testId="tokens-card">
<div
class="wrapper"
role="region"
aria-label={$i18n.portfolio.tokens_card_title}
>
<div class="header">
<div class="header-wrapper">
<div class="icon" aria-hidden="true">
<IconAccountsPage />
</div>
<div class="text-content">
<h5 class="title">{$i18n.portfolio.tokens_card_title}</h5>
<p
class="amount"
data-tid="amount"
aria-label={`${$i18n.portfolio.tokens_card_title}: ${usdAmount}`}
>
${usdAmountFormatted}
</p>
</div>
</div>
<a
{href}
class="button secondary"
aria-label={$i18n.portfolio.tokens_card_link}
>
<span class="mobile-only">
<IconRight />
</span>
<span class="tablet-up">
{$i18n.portfolio.tokens_card_link}
</span>
</a>
</div>
<div class="body" role="table">
<div class="tokens-header" role="row">
<span role="columnheader"
>{$i18n.portfolio.tokens_card_list_first_column}</span
>

<span class="mobile-only justify-end" role="columnheader"
>{$i18n.portfolio.tokens_card_list_second_column_mobile}</span
>
<span class="tablet-up justify-end" role="columnheader"
>{$i18n.portfolio.tokens_card_list_second_column}</span
>
<span class="tablet-up justify-end" role="columnheader"
>{$i18n.portfolio.tokens_card_list_third_column}</span
>
</div>

<div class="tokens-list" role="rowgroup">
{#each topTokens as token (token.domKey)}
<div class="token-row" data-tid="token-card-row" role="row">
<div class="token-info" role="cell">
<div>
<Logo src={token.logo} alt={token.title} size="medium" framed />
</div>
<span data-tid="token-title">{token.title}</span>
</div>

<div
class="token-native-balance"
data-tid="token-native-balance"
role="cell"
>
<AmountDisplay singleLine amount={token.balance} />
</div>
<div
class="token-usd-balance"
data-tid="token-usd-balance"
role="cell"
aria-label={`${token.title} USD: ${token?.balanceInUsd ?? 0}`}
>
${formatNumber(token?.balanceInUsd ?? 0)}
</div>
</div>
{/each}
{#if showInfoRow}
<div class="info-row desktop-only" role="note" data-tid="info-row">
<div class="icon" aria-hidden="true">
<IconAccountsPage />
</div>
<div class="message">
{$i18n.portfolio.tokens_card_info_row}
</div>
</div>
{/if}
</div>
</div>
</div>
</Card>

<style lang="scss">
@use "@dfinity/gix-components/dist/styles/mixins/media";
.wrapper {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--card-background-tint);
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--padding-3x) var(--padding-2x);
.header-wrapper {
display: flex;
align-items: flex-start;
gap: var(--padding-2x);
.icon {
width: 50px;
height: 50px;
}
.text-content {
display: flex;
flex-direction: column;
gap: var(--padding-0_5x);
.title {
font-size: 0.875rem;
font-weight: bold;
color: var(--text-description);
margin: 0;
padding: 0;
}
.amount {
font-size: 1.5rem;
}
}
}
}
.body {
display: flex;
flex-direction: column;
gap: var(--padding);
flex-grow: 1;
.tokens-header {
display: grid;
grid-template-columns: 1fr 1fr;
justify-content: space-between;
font-size: 0.875rem;
color: var(--text-description);
padding: 0 var(--padding-2x);
@include media.min-width(medium) {
grid-template-columns: 1fr 1fr 1fr;
}
}
.tokens-list {
display: flex;
flex-direction: column;
background-color: var(--card-background);
flex-grow: 1;
.token-row {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-areas:
"info usd"
"info balance";
@include media.min-width(medium) {
grid-template-columns: 1fr 1fr 1fr;
grid-template-areas: "info balance usd";
}
align-items: center;
padding: var(--padding-3x) var(--padding-2x);
border-top: 1px solid var(--elements-divider);
.token-info {
grid-area: info;
display: flex;
align-items: center;
gap: var(--padding);
}
.token-native-balance,
.token-usd-balance {
justify-self: end;
text-align: right;
}
.token-native-balance {
grid-area: balance;
font-size: 0.75rem;
@include media.min-width(medium) {
font-size: var(--font-size-standard);
}
}
.token-usd-balance {
grid-area: usd;
}
}
}
.info-row {
display: flex;
justify-content: center;
align-items: center;
gap: var(--padding-2x);
flex-grow: 1;
max-width: 90%;
margin: 0 auto;
.icon {
min-width: 50px;
height: 50px;
}
.message {
font-size: 0.875rem;
color: var(--text-description);
max-width: 400px;
}
}
}
/* Utilities */
.tablet-up,
.desktop-only {
display: none !important;
}
@include media.min-width(medium) {
.mobile-only {
display: none;
}
.tablet-up {
display: flex !important;
}
}
@include media.min-width(large) {
.desktop-only {
display: flex !important;
}
}
.justify-end {
justify-self: end;
}
}
</style>
2 changes: 2 additions & 0 deletions frontend/src/lib/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export const DAYS_IN_NON_LEAP_YEAR = 365;

export const NANO_SECONDS_IN_MILLISECOND = 1_000_000;
export const NANO_SECONDS_IN_MINUTE = NANO_SECONDS_IN_MILLISECOND * 1_000 * 60;

export const PRICE_NOT_AVAILABLE_PLACEHOLDER = "-/-";
9 changes: 8 additions & 1 deletion frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,13 @@
"no_tokens_card_description": "Vote on project and protocol developments and earn rewards - Ready to get started?",
"no_tokens_card_button": "Buy ICP",
"no_neurons_card_description": "Take part in voting for development decisions on SNS projects and earn rewards - start staking your neurons",
"no_neurons_card_button": "Start Staking"
"no_neurons_card_button": "Start Staking",
"tokens_card_title": "Total Token Balance",
"tokens_card_link": "View tokens",
"tokens_card_list_first_column": "Top Tokens Held",
"tokens_card_list_second_column_mobile": "Balance",
"tokens_card_list_second_column": "Value Native",
"tokens_card_list_third_column": "Value $",
"tokens_card_info_row": "Store and transfer tokens securely in the NNS wallet — running 100% on the Internet Computer blockchain. Tokens allow you to participate in ICP's onchain governance systems."
}
}
7 changes: 7 additions & 0 deletions frontend/src/lib/types/i18n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,13 @@ interface I18nPortfolio {
no_tokens_card_button: string;
no_neurons_card_description: string;
no_neurons_card_button: string;
tokens_card_title: string;
tokens_card_link: string;
tokens_card_list_first_column: string;
tokens_card_list_second_column_mobile: string;
tokens_card_list_second_column: string;
tokens_card_list_third_column: string;
tokens_card_info_row: string;
}

interface I18nNeuron_state {
Expand Down
Loading

0 comments on commit 90d8729

Please sign in to comment.