Skip to content

Commit

Permalink
feat: add token page
Browse files Browse the repository at this point in the history
  • Loading branch information
tx-nikola committed Jan 17, 2025
1 parent 83560f0 commit cc937f7
Show file tree
Hide file tree
Showing 9 changed files with 394 additions and 3 deletions.
9 changes: 7 additions & 2 deletions packages/app/src/components/AddressLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<a
v-if="network === 'L1' && !!currentNetwork.l1ExplorerUrl"
target="_blank"
:href="`${currentNetwork.l1ExplorerUrl}/address/${formattedAddress}`"
:href="`${currentNetwork.l1ExplorerUrl}/${props.isTokenAddress ? `token` : `address`}/${formattedAddress}`"
>
<slot>
{{ formattedAddress }}
Expand All @@ -13,7 +13,7 @@
{{ formattedAddress }}
</slot>
</span>
<router-link v-else :to="{ name: 'address', params: { address: formattedAddress } }">
<router-link v-else :to="{ name: props.isTokenAddress ? `token` : `address`, params: { address: formattedAddress } }">
<slot>
{{ formattedAddress }}
</slot>
Expand All @@ -40,6 +40,11 @@ const props = defineProps({
type: String as PropType<NetworkOrigin>,
default: "L2",
},
isTokenAddress: {
type: Boolean,
default: false,
required: false,
},
});
const { currentNetwork } = useContext();
Expand Down
253 changes: 253 additions & 0 deletions packages/app/src/components/Token.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
<template>
<div class="head-block">
<Breadcrumbs :items="breadcrumbItems" />
<SearchForm class="search-form" />
</div>
<div class="flex gap-2 items-center justify-start">
<div class="token-icon-container">
<img
v-if="contract?.address && !pending && tokenInfo"
class="token-img"
:src="tokenInfo.iconURL ?? undefined"
:alt="tokenInfo.symbol || t('balances.table.unknownSymbol')"
/>
</div>

<Title
v-if="contract?.address && !pending"
:title="
tokenInfo?.name && tokenInfo?.symbol
? `${tokenInfo?.name} (${tokenInfo?.symbol})`
: contractName ?? t('contract.title')
"
:value="contractName ? undefined : contract?.address"
:is-verified="contract?.verificationInfo != null"
:is-evm-like="contract?.isEvmLike"
/>
<Spinner v-else size="md" />
</div>
<div class="tables-container">
<div class="contract-tables-container">
<div>
<ContractInfoTable class="contract-info-table" :loading="pending" :contract="contract!" />
</div>
<div>
<BalanceTable class="balance-table" :loading="pending" :balances="contract?.balances">
<template #not-found>
<EmptyState>
<template #image>
<div class="balances-empty-icon">
<img src="/images/empty-state/empty_balance.svg" alt="empty_balance" />
</div>
</template>
<template #title>
{{ t("contract.balances.notFound.title") }}
</template>
<template #description>
<div class="balances-empty-description">{{ t("contract.balances.notFound.subtitle") }}</div>
</template>
</EmptyState>
</template>
<template #failed>
<EmptyState>
<template #image>
<div class="balances-empty-icon">
<img src="/images/empty-state/error_balance.svg" alt="empty_balance" />
</div>
</template>
<template #title>
{{ t("contract.balances.error.title") }}
</template>
<template #description>
<div class="balances-empty-description">{{ t("contract.balances.error.subtitle") }}</div>
</template>
</EmptyState>
</template>
</BalanceTable>
</div>
</div>

<Tabs v-if="contract?.address && !pending" class="contract-tabs" :tabs="tabs">
<template #tab-1-content>
<TransactionsTable
class="transactions-table"
:search-params="transactionsSearchParams"
:contract-abi="contractABI"
>
<template #not-found>
<TransactionEmptyState />
</template>
</TransactionsTable>
</template>
<template #tab-2-content>
<TransfersTable :address="contract.address" />
</template>
<template #tab-3-content>
<ContractInfoTab :contract="contract" />
</template>
<template #tab-4-content>
<ContractEvents :contract="contract" />
</template>
</Tabs>
</div>
</template>
<script lang="ts" setup>
import { computed, type PropType, watchEffect } from "vue";
import { useI18n } from "vue-i18n";
import { CheckCircleIcon } from "@heroicons/vue/solid";
import SearchForm from "@/components/SearchForm.vue";
import BalanceTable from "@/components/balances/Table.vue";
import Breadcrumbs from "@/components/common/Breadcrumbs.vue";
import EmptyState from "@/components/common/EmptyState.vue";
import Spinner from "@/components/common/Spinner.vue";
import Tabs from "@/components/common/Tabs.vue";
import Title from "@/components/common/Title.vue";
import ContractInfoTab from "@/components/contract/ContractInfoTab.vue";
import ContractInfoTable from "@/components/contract/InfoTable.vue";
import TransactionEmptyState from "@/components/contract/TransactionEmptyState.vue";
import ContractEvents from "@/components/event/ContractEvents.vue";
import TransactionsTable from "@/components/transactions/Table.vue";
import TransfersTable from "@/components/transfers/Table.vue";
import useToken from "@/composables/useToken";
import type { BreadcrumbItem } from "@/components/common/Breadcrumbs.vue";
import type { Contract } from "@/composables/useAddress";
import { shortValue } from "@/utils/formatters";
const { t } = useI18n();
const props = defineProps({
contract: {
type: [Object, null] as PropType<Contract | null>,
required: true,
default: null,
},
pending: {
type: Boolean,
default: false,
},
failed: {
type: Boolean,
default: false,
},
});
const { getTokenInfo, tokenInfo } = useToken();
watchEffect(() => {
if (props.contract) {
getTokenInfo(props.contract.address);
}
});
const tabs = computed(() => [
{ title: t("tabs.transactions"), hash: "#transactions" },
{ title: t("tabs.transfers"), hash: "#transfers" },
{
title: t("tabs.contract"),
hash: "#contract",
icon: props.contract?.verificationInfo ? CheckCircleIcon : null,
},
{ title: t("tabs.events"), hash: "#events" },
]);
const breadcrumbItems = computed((): BreadcrumbItem[] | [] => {
if (props.contract?.address) {
return [
{ to: { name: "home" }, text: t("breadcrumbs.home") },
{
text: `${t("contract.contractNumber")}${shortValue(props.contract?.address)}`,
},
];
}
return [];
});
const contractName = computed(() => props.contract?.verificationInfo?.request.contractName.replace(/.*\.sol:/, ""));
const contractABI = computed(() => props.contract?.verificationInfo?.artifacts.abi);
const transactionsSearchParams = computed(() => ({
address: props.contract?.address,
}));
</script>
<style lang="scss" scoped>
.head-block {
@apply mb-8 flex flex-col-reverse justify-between lg:mb-10 lg:flex-row;
h1 {
@apply mt-3;
}
.search-form {
@apply mb-6 w-full max-w-[26rem] lg:mb-0;
}
}
.tables-container {
@apply mt-8 grid grid-cols-1 gap-4;
.contract-tabs {
@apply shadow-md;
}
.contract-tables-container {
@apply grid grid-cols-1 gap-4 lg:grid-cols-2;
}
h2 {
@apply mb-4;
}
.contract-info-table {
@apply mb-8 overflow-hidden;
}
.transactions-table {
@apply shadow-none;
.table-body {
@apply rounded-none;
}
}
.balance-table {
@apply mb-4 overflow-hidden bg-white;
.balances-empty-icon {
@apply m-auto;
img {
@apply w-[2.875rem];
}
}
.balances-empty-description {
@apply max-w-[16rem] whitespace-normal;
}
}
}
.transaction-table-error {
@apply text-2xl text-error-700;
}
.token-icon-container {
@apply relative overflow-hidden rounded-full h-8 w-8;
.token-img-loader,
.token-img {
@apply absolute inset-0 h-full w-full rounded-full;
}
.token-img-loader {
@apply animate-pulse bg-neutral-200;
}
.token-img {
@apply transition-opacity duration-150;
}
}
</style>

<style lang="scss">
.contract-tabs {
.tab-head {
@apply overflow-auto;
}
.transactions-table {
.table-body {
@apply rounded-t-none;
}
table thead tr th {
@apply first:rounded-none last:rounded-none;
}
}
}
</style>
8 changes: 7 additions & 1 deletion packages/app/src/components/TokenIconLabel.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<template>
<div class="token-icon-label">
<AddressLink :address="address" class="token-link" :data-testid="$testId?.tokensIcon">
<AddressLink
:address="address"
class="token-link"
:data-testid="$testId?.tokensIcon"
:is-token-address="address.toLowerCase() === L2_BASE_TOKEN_ADDRESS ? false : true"
>
<span v-if="showLinkSymbol" class="token-symbol">
<span v-if="symbol">
{{ symbol }}
Expand Down Expand Up @@ -32,6 +37,7 @@ import { computed, type PropType } from "vue";
import { useI18n } from "vue-i18n";
import { useImage } from "@vueuse/core";
import { L2_BASE_TOKEN_ADDRESS } from "zksync-ethers/build/utils";
import AddressLink from "@/components/AddressLink.vue";
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/components/token/TokenListTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<AddressLink
:data-testid="$testId.tokenAddress"
:address="item.l2Address"
:is-token-address="item.l2Address.toLowerCase() === L2_BASE_TOKEN_ADDRESS ? false : true"
class="token-address block max-w-sm"
>
{{ shortenFitText(item.l2Address, "left", 210, subtraction) }}
Expand Down Expand Up @@ -66,6 +67,7 @@ import { type PropType, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useElementSize } from "@vueuse/core";
import { L2_BASE_TOKEN_ADDRESS } from "zksync-ethers/build/utils";
import AddressLink from "@/components/AddressLink.vue";
import TokenIconLabel from "@/components/TokenIconLabel.vue";
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@
"addressView": {
"title": "Address"
},
"tokenView": {
"title": "Token"
},
"contract": {
"title": "Contract",
"contractNumber": "Contract ",
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/locales/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,9 @@
"addressView": {
"title": "Адреса"
},
"tokenView": {
"title": "Токен"
},
"accountView": {
"title": "Профіль",
"infoTableError": "Виникла помилка",
Expand Down
9 changes: 9 additions & 0 deletions packages/app/src/router/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ export default [
title: "addressView.title",
},
},
{
path: "/token/:address",
name: "token",
component: () => import("@/views/TokenView.vue"),
props: true,
meta: {
title: "tokenView.title",
},
},
{
path: "/contracts/verify",
name: "contract-verification",
Expand Down
Loading

0 comments on commit cc937f7

Please sign in to comment.