web/multinode: wallets page markup

Change-Id: I01cb973196d4be8d5cc959a3129c238236736538
This commit is contained in:
NickolaiYurchenko 2021-05-25 23:14:01 +03:00
parent 21731ff8d0
commit c0b170149d
19 changed files with 962 additions and 9 deletions

View File

@ -0,0 +1,65 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="pages-container">
<span
class="pages-container__pages"
v-for="page in pages"
:class="{ 'selected': isSelected(page.index) }"
@click="page.select()"
:key="page.index">{{ page.index }}</span>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { CheckSelected, Page } from '@/app/types/pagination';
@Component
export default class PagesBlock extends Vue {
@Prop({default: () => []})
public readonly pages: Page[];
@Prop({default: () => false})
public readonly isSelected: CheckSelected;
}
</script>
<style scoped lang="scss">
.pages-container {
display: flex;
&__pages {
font-family: 'font_medium', sans-serif;
font-size: 16px;
margin-right: 8px;
text-align: center;
cursor: pointer;
position: relative;
border: 1px solid #e1e3e6;
border-radius: 6px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&:hover {
color: #2379ec;
border-color: #2379ec;
}
&:last-child {
margin-right: 0;
}
}
}
.selected {
color: #2379ec;
font-family: 'font_bold', sans-serif;
border-color: #2379ec;
}
</style>

View File

@ -0,0 +1,316 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="pagination-container">
<div class="pagination-container__pages">
<div class="pagination-container__button" @click="prevPage">
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.80078 9.2625L15.5258 15L9.80078 20.7375L11.5633 22.5L19.0633 15L11.5633 7.5L9.80078 9.2625Z" fill="#586474"/>
</svg>
</div>
<div class="pagination-container__items">
<PagesBlock
:pages="firstBlockPages"
:is-selected="isSelected"
/>
<span class="pages-divider" v-if="isFirstDotsShown">...</span>
<PagesBlock
:pages="middleBlockPages"
:is-selected="isSelected"
/>
<span class="pages-divider" v-if="isSecondDotsShown">...</span>
<PagesBlock
:pages="lastBlockPages"
:is-selected="isSelected"
/>
</div>
<div class="pagination-container__button" @click="nextPage">
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.80078 9.2625L15.5258 15L9.80078 20.7375L11.5633 22.5L19.0633 15L11.5633 7.5L9.80078 9.2625Z" fill="#586474"/>
</svg>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import PagesBlock from '@/app/components/common/PagesBlock.vue';
import { OnPageClickCallback, Page } from '@/app/types/pagination';
@Component({
components: {
PagesBlock,
},
})
export default class VPagination extends Vue {
private readonly MAX_PAGES_PER_BLOCK: number = 3;
private readonly MAX_PAGES_OFF_BLOCKS: number = 6;
private currentPageNumber: number = 1;
public isLoading = false;
public pagesArray: Page[] = [];
public firstBlockPages: Page[] = [];
public middleBlockPages: Page[] = [];
public lastBlockPages: Page[] = [];
@Prop({default: 0})
private readonly totalPageCount: number;
@Prop({default: () => new Promise(() => false)})
private readonly onPageClickCallback: OnPageClickCallback;
/**
* Component initialization.
*/
public mounted() {
this.populatePagesArray();
}
/**
* Indicates if current page is first.
*/
public get isFirstPage(): boolean {
return this.currentPageNumber === 1;
}
/**
* Indicates if current page is last.
*/
public get isLastPage(): boolean {
return this.currentPageNumber === this.totalPageCount;
}
/**
* Indicates if dots after first pages block should appear.
*/
public get isFirstDotsShown(): boolean {
return this.middleBlockPages.length <= this.MAX_PAGES_PER_BLOCK
&& this.pagesArray.length > this.MAX_PAGES_OFF_BLOCKS;
}
/**
* Indicates if dots after middle pages block should appear.
*/
public get isSecondDotsShown(): boolean {
return !!this.middleBlockPages.length;
}
/**
* Indicates page is current and should appear in different styling.
*/
public isSelected(page: number): boolean {
return page === this.currentPageNumber;
}
/**
* Method after total page count change.
*/
@Watch('totalPageCount')
public onPageCountChange(val: number, oldVal: number) {
this.resetPageIndex();
}
/**
* onPageClick fires after concrete page click.
*/
public async onPageClick(page: number): Promise<void> {
if (this.isLoading) {
return;
}
this.isLoading = true;
await this.onPageClickCallback(page);
this.setCurrentPage(page);
this.reorganizePageBlocks();
this.isLoading = false;
}
/**
* nextPage fires after 'next' arrow click.
*/
public async nextPage(): Promise<void> {
if (this.isLastPage || this.isLoading) {
return;
}
this.isLoading = true;
await this.onPageClickCallback(this.currentPageNumber + 1);
this.incrementCurrentPage();
this.reorganizePageBlocks();
this.isLoading = false;
}
/**
* prevPage fires after 'previous' arrow click.
*/
public async prevPage(): Promise<void> {
if (this.isFirstPage || this.isLoading) {
return;
}
this.isLoading = true;
await this.onPageClickCallback(this.currentPageNumber - 1);
this.decrementCurrentPage();
this.reorganizePageBlocks();
this.isLoading = false;
}
/**
* resetPageIndex sets current selected page as first and rebuilds page blocks after.
*/
public resetPageIndex(): void {
this.pagesArray = [];
this.firstBlockPages = [];
this.setCurrentPage(1);
this.populatePagesArray();
}
/**
* creates pages blocks and pages depends of total page count.
*/
private populatePagesArray(): void {
if (!this.totalPageCount) {
return;
}
for (let i = 1; i <= this.totalPageCount; i++) {
this.pagesArray.push(new Page(i, this.onPageClick));
}
if (this.isPagesTotalOffBlocks()) {
this.firstBlockPages = this.pagesArray.slice();
this.middleBlockPages = [];
this.lastBlockPages = [];
return;
}
this.reorganizePageBlocks();
}
/**
* reorganizePageBlocks changes pages blocks organization depends of
* current selected page index.
*/
private reorganizePageBlocks(): void {
if (this.isPagesTotalOffBlocks()) {
return;
}
if (this.isCurrentInFirstBlock()) {
this.setBlocksIfCurrentInFirstBlock();
return;
}
if (!this.isCurrentInFirstBlock() && !this.isCurrentInLastBlock()) {
this.setBlocksIfCurrentInMiddleBlock();
return;
}
if (this.isCurrentInLastBlock()) {
this.setBlocksIfCurrentInLastBlock();
}
}
private setBlocksIfCurrentInFirstBlock(): void {
this.firstBlockPages = this.pagesArray.slice(0, 3);
this.middleBlockPages = [];
this.lastBlockPages = this.pagesArray.slice(-1);
}
private setBlocksIfCurrentInMiddleBlock(): void {
this.firstBlockPages = this.pagesArray.slice(0, 1);
this.middleBlockPages = this.pagesArray.slice(this.currentPageNumber - 2, this.currentPageNumber + 1);
this.lastBlockPages = this.pagesArray.slice(-1);
}
private setBlocksIfCurrentInLastBlock(): void {
this.firstBlockPages = this.pagesArray.slice(0, 1);
this.middleBlockPages = [];
this.lastBlockPages = this.pagesArray.slice(-3);
}
private isCurrentInFirstBlock(): boolean {
return this.currentPageNumber < this.MAX_PAGES_PER_BLOCK;
}
private isCurrentInLastBlock(): boolean {
return this.totalPageCount - this.currentPageNumber < this.MAX_PAGES_PER_BLOCK - 1;
}
private isPagesTotalOffBlocks(): boolean {
return this.totalPageCount <= this.MAX_PAGES_OFF_BLOCKS;
}
private incrementCurrentPage(): void {
this.currentPageNumber++;
}
private decrementCurrentPage(): void {
this.currentPageNumber--;
}
private setCurrentPage(pageNumber: number): void {
this.currentPageNumber = pageNumber;
}
}
</script>
<style scoped lang="scss">
.pagination-container {
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 25px;
margin-top: 25px;
&__pages {
display: flex;
align-items: center;
}
&__button {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: 1px solid #e1e3e6;
border-radius: 6px;
width: 40px;
height: 40px;
&:first-of-type {
svg {
transform: rotate(180deg);
}
}
&:hover {
color: #2379ec;
border-color: #2379ec;
}
}
&__items {
margin: 0 20px;
display: flex;
.pages-divider {
border: 1px solid #e1e3e6;
border-radius: 6px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 20px;
}
}
}
</style>

View File

@ -54,7 +54,8 @@ export default class NavigationArea extends Vue {
* Array of navigation links with icons. * Array of navigation links with icons.
*/ */
public readonly navigation: NavigationLink[] = [ public readonly navigation: NavigationLink[] = [
new NavigationLink('My Nodes', RouterConfig.MyNodes.path, MyNodesIcon), new NavigationLink(RouterConfig.MyNodes.name, RouterConfig.MyNodes.path, MyNodesIcon),
new NavigationLink(RouterConfig.Wallets.name, RouterConfig.Wallets.with(RouterConfig.WalletsSummary).path, PayoutsIcon),
new NavigationLink(RouterConfig.Payouts.name, RouterConfig.Payouts.path, PayoutsIcon), new NavigationLink(RouterConfig.Payouts.name, RouterConfig.Payouts.path, PayoutsIcon),
new NavigationLink(RouterConfig.Bandwidth.name, RouterConfig.Bandwidth.path, TrafficIcon), new NavigationLink(RouterConfig.Bandwidth.name, RouterConfig.Bandwidth.path, TrafficIcon),
new NavigationLink('Reputation', '/reputation', ReputationIcon), new NavigationLink('Reputation', '/reputation', ReputationIcon),

View File

@ -22,7 +22,7 @@ import PayoutPeriodCalendar from './PayoutPeriodCalendar.vue';
@Component({ @Component({
components: { PayoutPeriodCalendar }, components: { PayoutPeriodCalendar },
}) })
export default class Payout extends Vue { export default class PayoutPeriodCalendarButton extends Vue {
@Prop({default: ''}) @Prop({default: ''})
public period: string; public period: string;

View File

@ -0,0 +1,28 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<base-table >
<thead slot="head">
<tr>
<th class="align-left">NODE</th>
<th>UNDISTRIBUTED</th>
</tr>
</thead>
<tbody slot="body">
<tr class="table-item">
<th class="align-left">My New Node</th>
<th>{{ 2000 | centsToDollars }}</th>
</tr>
</tbody>
</base-table>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import BaseTable from '@/app/components/common/BaseTable.vue';
@Component({ components: { BaseTable } })
export default class WalletDetailsTable extends Vue {}
</script>

View File

@ -0,0 +1,135 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<base-table >
<thead slot="head">
<tr>
<th class="align-left">WALLET ADDRESS</th>
<th>UNDISTRIBUTED</th>
<th class="align-left">VIEW</th>
</tr>
</thead>
<tbody slot="body">
<tr class="table-item">
<th class="align-left">
<div class="column">
<p class="table-item__wallet" @click.prevent="redirectToWalletDetailsPage">
{{ '0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac' }}
</p>
<div class="table-item__wallet-feature" :class="{ 'active': false }">
<template v-if="false">
<svg class="table-item__wallet-feature__icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.5 8C15.5 9.98912 14.7098 11.8968 13.3033 13.3033C11.8968 14.7098 9.98912 15.5 8 15.5C6.01088 15.5 4.10322 14.7098 2.6967 13.3033C1.29018 11.8968 0.5 9.98912 0.5 8C0.5 6.01088 1.29018 4.10322 2.6967 2.6967C4.10322 1.29018 6.01088 0.5 8 0.5C9.98912 0.5 11.8968 1.29018 13.3033 2.6967C14.7098 4.10322 15.5 6.01088 15.5 8ZM11.7781 5.15937C11.7112 5.09264 11.6314 5.0401 11.5437 5.00489C11.4559 4.96968 11.362 4.95252 11.2675 4.95445C11.173 4.95637 11.0798 4.97734 10.9936 5.0161C10.9073 5.05485 10.8298 5.1106 10.7656 5.18L7.50969 9.32844L5.5475 7.36531C5.41421 7.24111 5.23792 7.1735 5.05576 7.17671C4.8736 7.17992 4.6998 7.25372 4.57098 7.38254C4.44215 7.51137 4.36836 7.68517 4.36515 7.86732C4.36193 8.04948 4.42955 8.22577 4.55375 8.35906L7.03437 10.8406C7.1012 10.9073 7.18078 10.9599 7.26836 10.9952C7.35594 11.0305 7.44973 11.0477 7.54414 11.046C7.63854 11.0442 7.73163 11.0235 7.81784 10.985C7.90405 10.9465 7.98163 10.891 8.04594 10.8219L11.7884 6.14375C11.916 6.01109 11.9865 5.8337 11.9848 5.64965C11.983 5.4656 11.9092 5.28958 11.7791 5.15937H11.7781Z" fill="#00CE7D"/>
</svg>
<p class="table-item__wallet-feature__label">zkSync is opted-in</p>
</template>
<template v-else>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5 9C16.5 10.9891 15.7098 12.8968 14.3033 14.3033C12.8968 15.7098 10.9891 16.5 9 16.5C7.01088 16.5 5.10322 15.7098 3.6967 14.3033C2.29018 12.8968 1.5 10.9891 1.5 9C1.5 7.01088 2.29018 5.10322 3.6967 3.6967C5.10322 2.29018 7.01088 1.5 9 1.5C10.9891 1.5 12.8968 2.29018 14.3033 3.6967C15.7098 5.10322 16.5 7.01088 16.5 9Z" fill="#586474"/>
<rect x="5" y="8.30078" width="8" height="1.5" rx="0.75" fill="white"/>
</svg>
<p class="table-item__wallet-feature__label">zkSync is opted-out</p>
</template>
</div>
</div>
</th>
<th>{{ 2000 | centsToDollars }}</th>
<th class="align-left">
<div class="column">
<v-link uri="#" label="View on Etherscan" />
<v-link uri="#" label="View on zkScan" />
</div>
</th>
</tr>
</tbody>
</base-table>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import BaseTable from '@/app/components/common/BaseTable.vue';
import VLink from '@/app/components/common/VLink.vue';
import { Config as RouterConfig } from '@/app/router';
@Component({
components: {
VLink,
BaseTable,
},
})
export default class WalletsTable extends Vue {
public redirectToWalletDetailsPage(): void {
this.$router.push({
name: RouterConfig.Wallets.with(RouterConfig.WalletDetails).name,
params: { address: 'address' },
});
}
}
</script>
<style lang="scss" scoped>
.column {
display: flex;
flex-direction: column;
align-items: flex-start;
& .link:not(:first-of-type) {
margin-top: 10px;
}
}
.table-item {
box-sizing: border-box;
height: 89px;
border: 1px solid var(--c-gray--light);
cursor: pointer;
&__wallet {
font-family: 'font_medium', sans-serif;
font-size: 16px;
color: var(--c-primary);
}
&__wallet-feature {
margin-top: 10px;
display: flex;
align-items: center;
justify-content: flex-start;
&__icon {
background: white;
border-radius: 50%;
path {
fill: var(--c-title);
}
}
&__label {
font-family: 'font_semiBold', sans-serif;
font-size: 14px;
line-height: 17px;
margin-left: 7.5px;
color: var(--c-label);
}
&.active {
svg {
path {
fill: var(--wallet-feature-opted-in);
}
}
p {
color: var(--wallet-feature-opted-in);
}
}
}
}
</style>

View File

@ -7,10 +7,13 @@ import { Component } from 'vue-router/types/router';
import AddFirstNode from '@/app/views/AddFirstNode.vue'; import AddFirstNode from '@/app/views/AddFirstNode.vue';
import BandwidthPage from '@/app/views/bandwidth/BandwidthPage.vue'; import BandwidthPage from '@/app/views/bandwidth/BandwidthPage.vue';
import Dashboard from '@/app/views/Dashboard.vue'; import Dashboard from '@/app/views/Dashboard.vue';
import MyNodes from '@/app/views/MyNodes.vue'; import MyNodes from '@/app/views/myNodes/MyNodes.vue';
import PayoutsByNode from '@/app/views/PayoutsByNode.vue'; import PayoutsByNode from '@/app/views/payouts/PayoutsByNode.vue';
import PayoutsPage from '@/app/views/PayoutsPage.vue'; import PayoutsPage from '@/app/views/payouts/PayoutsPage.vue';
import PayoutsRoot from '@/app/views/PayoutsRoot.vue'; import PayoutsRoot from '@/app/views/payouts/PayoutsRoot.vue';
import WalletDetailsPage from '@/app/views/wallets/WalletDetailsPage.vue';
import WalletsPage from '@/app/views/wallets/WalletsPage.vue';
import WalletsRoot from '@/app/views/wallets/WalletsRoot.vue';
import WelcomeScreen from '@/app/views/WelcomeScreen.vue'; import WelcomeScreen from '@/app/views/WelcomeScreen.vue';
/** /**
@ -62,10 +65,17 @@ export class Route {
return this; return this;
} }
/**
* indicates if this route is a child route.
*/
public isChild(): boolean { public isChild(): boolean {
return this.path[0] !== '/'; return this.path[0] !== '/';
} }
/**
* combines child route with its ancestor.
* @param child
*/
public with(child: Route): Route { public with(child: Route): Route {
if (!child.isChild()) { if (!child.isChild()) {
throw new Error('provided child root is not defined'); throw new Error('provided child root is not defined');
@ -81,12 +91,19 @@ export class Route {
export class Config { export class Config {
public static Root: Route = new Route('/', 'Root', Dashboard, {requiresAuth: true}); public static Root: Route = new Route('/', 'Root', Dashboard, {requiresAuth: true});
public static Welcome: Route = new Route('/welcome', 'Welcome', WelcomeScreen); public static Welcome: Route = new Route('/welcome', 'Welcome', WelcomeScreen);
// nodes.
public static AddFirstNode: Route = new Route('/add-first-node', 'AddFirstNode', AddFirstNode); public static AddFirstNode: Route = new Route('/add-first-node', 'AddFirstNode', AddFirstNode);
public static MyNodes: Route = new Route('/my-nodes', 'MyNodes', MyNodes); public static MyNodes: Route = new Route('/my-nodes', 'MyNodes', MyNodes);
// payouts.
public static PayoutsSummary: Route = new Route('summary', 'PayoutsSummary', PayoutsPage); public static PayoutsSummary: Route = new Route('summary', 'PayoutsSummary', PayoutsPage);
public static PayoutsByNode: Route = new Route('by-node/:id', 'PayoutsByNode', PayoutsByNode); public static PayoutsByNode: Route = new Route('by-node/:id', 'PayoutsByNode', PayoutsByNode);
public static Payouts: Route = new Route('/payouts', 'Payouts', PayoutsRoot, undefined, Config.PayoutsSummary); public static Payouts: Route = new Route('/payouts', 'Payouts', PayoutsRoot, undefined, Config.PayoutsSummary);
// bandwidth and disk.
public static Bandwidth: Route = new Route('/bandwidth', 'Bandwidth & Disk', BandwidthPage); public static Bandwidth: Route = new Route('/bandwidth', 'Bandwidth & Disk', BandwidthPage);
// wallets.
public static WalletsSummary: Route = new Route('summary', 'WalletsSummary', WalletsPage);
public static WalletDetails: Route = new Route('details/:address', 'WalletDetails', WalletDetailsPage);
public static Wallets: Route = new Route('/wallets', 'Wallets', WalletsRoot, undefined, Config.WalletsSummary);
public static mode: RouterMode = 'history'; public static mode: RouterMode = 'history';
public static routes: Route[] = [ public static routes: Route[] = [
@ -96,6 +113,10 @@ export class Config {
Config.PayoutsByNode, Config.PayoutsByNode,
Config.PayoutsSummary, Config.PayoutsSummary,
]), ]),
Config.Wallets.addChildren([
Config.WalletDetails,
Config.WalletsSummary,
]),
Config.Bandwidth, Config.Bandwidth,
]), ]),
Config.Welcome, Config.Welcome,

View File

@ -0,0 +1,27 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
export type OnPageClickCallback = (search: number) => Promise<void>;
export type CheckSelected = (index: number) => boolean;
/**
* Describes paginator page.
*/
export class Page {
private readonly pageIndex: number = 1;
private readonly onClick: OnPageClickCallback;
constructor(index: number, callback: OnPageClickCallback) {
this.pageIndex = index;
this.onClick = callback;
}
public get index() {
return this.pageIndex;
}
public async select(): Promise<void> {
await this.onClick(this.pageIndex);
}
}

View File

@ -0,0 +1,205 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="wallet-details">
<div class="wallet-details__header">
<div class="wallet-details__header__left-area">
<div class="wallet-details__header__left-area__title-area">
<div class="wallet-details__header__left-area__title-area__arrow" @click="redirectToWalletsSummary">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3398 0.554956C14.0797 1.2949 14.0797 2.49458 13.3398 3.23452L6.46904 10.1053H22.1053C23.1517 10.1053 24 10.9536 24 12C24 13.0464 23.1517 13.8947 22.1053 13.8947H6.46904L13.3398 20.7655C14.0797 21.5054 14.0797 22.7051 13.3398 23.445C12.5998 24.185 11.4002 24.185 10.6602 23.445L0.554956 13.3398C-0.184985 12.5998 -0.184985 11.4002 0.554956 10.6602L10.6602 0.554956C11.4002 -0.184985 12.5998 -0.184985 13.3398 0.554956Z" fill="#252A32"/>
</svg>
</div>
<h1 class="wallet-details__header__left-area__title-area__title">Wallet</h1>
</div>
<p class="wallet-details__header__left-area__wallet">{{ '0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac' }}</p>
<div class="wallet-details__header__left-area__wallet-feature" :class="{ 'active': false }">
<template v-if="false">
<svg class="wallet-details__header__left-area__wallet-feature__icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.5 8C15.5 9.98912 14.7098 11.8968 13.3033 13.3033C11.8968 14.7098 9.98912 15.5 8 15.5C6.01088 15.5 4.10322 14.7098 2.6967 13.3033C1.29018 11.8968 0.5 9.98912 0.5 8C0.5 6.01088 1.29018 4.10322 2.6967 2.6967C4.10322 1.29018 6.01088 0.5 8 0.5C9.98912 0.5 11.8968 1.29018 13.3033 2.6967C14.7098 4.10322 15.5 6.01088 15.5 8ZM11.7781 5.15937C11.7112 5.09264 11.6314 5.0401 11.5437 5.00489C11.4559 4.96968 11.362 4.95252 11.2675 4.95445C11.173 4.95637 11.0798 4.97734 10.9936 5.0161C10.9073 5.05485 10.8298 5.1106 10.7656 5.18L7.50969 9.32844L5.5475 7.36531C5.41421 7.24111 5.23792 7.1735 5.05576 7.17671C4.8736 7.17992 4.6998 7.25372 4.57098 7.38254C4.44215 7.51137 4.36836 7.68517 4.36515 7.86732C4.36193 8.04948 4.42955 8.22577 4.55375 8.35906L7.03437 10.8406C7.1012 10.9073 7.18078 10.9599 7.26836 10.9952C7.35594 11.0305 7.44973 11.0477 7.54414 11.046C7.63854 11.0442 7.73163 11.0235 7.81784 10.985C7.90405 10.9465 7.98163 10.891 8.04594 10.8219L11.7884 6.14375C11.916 6.01109 11.9865 5.8337 11.9848 5.64965C11.983 5.4656 11.9092 5.28958 11.7791 5.15937H11.7781Z" fill="#00CE7D"/>
</svg>
<p class="wallet-details__header__left-area__wallet-feature__label">zkSync is opted-in</p>
</template>
<template v-else>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5 9C16.5 10.9891 15.7098 12.8968 14.3033 14.3033C12.8968 15.7098 10.9891 16.5 9 16.5C7.01088 16.5 5.10322 15.7098 3.6967 14.3033C2.29018 12.8968 1.5 10.9891 1.5 9C1.5 7.01088 2.29018 5.10322 3.6967 3.6967C5.10322 2.29018 7.01088 1.5 9 1.5C10.9891 1.5 12.8968 2.29018 14.3033 3.6967C15.7098 5.10322 16.5 7.01088 16.5 9Z" fill="#586474"/>
<rect x="5" y="8.30078" width="8" height="1.5" rx="0.75" fill="white"/>
</svg>
<p class="wallet-details__header__left-area__wallet-feature__label">zkSync is opted-out</p>
</template>
</div>
</div>
<div class="wallet-details__header__right-area">
<p class="wallet-details__header__right-area__label">Undistributed Balance</p>
<p class="wallet-details__header__right-area__value">{{ 25059 | centsToDollars }}</p>
<v-link uri="#" label="View on Etherscan" />
<v-link uri="#" label="View on zkScan" />
</div>
</div>
<div class="wallet-details__content">
<h2 class="wallet-details__content__title">Connected Nodes</h2>
<wallet-details-table />
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import VLink from '@/app/components/common/VLink.vue';
import WalletDetailsTable from '@/app/components/wallets/tables/walletDetails/WalletDetailsTable.vue';
import { Config as RouterConfig } from '@/app/router';
@Component({
components: {
WalletDetailsTable,
VLink,
},
})
export default class WalletDetailsPage extends Vue {
/**
* Checks id address parameters and redirects if no provided.
*/
public beforeMount(): void {
if (!this.$route.params.address) {
this.redirectToWalletsSummary();
}
}
public redirectToWalletsSummary(): void {
this.$router.push(RouterConfig.Wallets.with(RouterConfig.WalletsSummary).path);
}
}
</script>
<style lang="scss" scoped>
.wallet-details {
box-sizing: border-box;
padding: 60px;
overflow-y: auto;
height: calc(100vh - 60px);
color: var(--c-title);
&__header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding-bottom: 32px;
border-bottom: 1px solid var(--c-gray--light);
&__left-area {
display: flex;
flex-direction: column;
align-items: flex-start;
&__title-area {
font-family: 'font_bold', sans-serif;
font-size: 32px;
display: flex;
align-items: center;
justify-content: flex-start;
&__arrow {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
max-width: 32px;
max-height: 32px;
cursor: pointer;
margin-right: 20px;
}
&__title {
font-family: 'font_bold', sans-serif;
font-size: 32px;
white-space: nowrap;
text-overflow: ellipsis;
position: relative;
overflow: hidden;
}
}
&__wallet {
font-family: 'font_bold', sans-serif;
font-size: 18px;
margin-top: 15px;
}
&__wallet-feature {
margin-top: 15px;
display: flex;
align-items: center;
justify-content: flex-start;
&__icon {
background: white;
border-radius: 50%;
path {
fill: var(--c-title);
}
}
&__label {
font-family: 'font_semiBold', sans-serif;
font-size: 16px;
margin-left: 7.5px;
color: var(--c-label);
}
&.active {
svg {
path {
fill: var(--wallet-feature-opted-in);
}
}
p {
color: var(--wallet-feature-opted-in);
}
}
}
}
&__right-area {
display: flex;
flex-direction: column;
align-items: flex-end;
&__label {
font-family: 'font_medium', sans-serif;
font-size: 14px;
color: var(--c-label);
}
&__value {
font-family: 'font_bold', sans-serif;
font-size: 32px;
margin-top: 8px;
margin-bottom: 20px;
}
& .link {
margin-top: 10px;
}
}
}
&__content {
margin-top: 36px;
&__title {
font-family: 'font_bold', sans-serif;
font-size: 24px;
margin-bottom: 24px;
}
}
}
</style>

View File

@ -0,0 +1,140 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="wallets">
<h1 class="wallets__title">Wallets</h1>
<div class="wallets__content-area">
<div class="wallets__left-area">
<wallets-table
class="wallets__left-area__table"
/>
</div>
<div class="wallets__right-area">
<info-block class="information">
<div class="wallets__information" slot="body">
<h3 class="wallets__information__title">Payouts with zkSync</h3>
<p class="wallets__information__description">Short description how minimal threshold system works.</p>
<v-link uri="#" label="Learn more" />
</div>
</info-block>
</div>
</div>
<div class="wallets__pagination">
<v-pagination :total-page-count="10" />
<p class="wallets__pagination__info">Showing <strong>{{ 6 }} of {{ 200 }}</strong> wallets</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import InfoBlock from '@/app/components/common/InfoBlock.vue';
import VLink from '@/app/components/common/VLink.vue';
import VPagination from '@/app/components/common/VPagination.vue';
import WalletsTable from '@/app/components/wallets/tables/walletsSummary/WalletsTable.vue';
import { UnauthorizedError } from '@/api';
@Component({
components: {
VPagination,
VLink,
InfoBlock,
WalletsTable,
},
})
export default class WalletsPage extends Vue {
public mounted(): void {
try {
// api call here
} catch (error) {
if (error instanceof UnauthorizedError) {
// TODO: redirect to login screen.
}
// TODO: notify error
}
}
}
</script>
<style lang="scss" scoped>
.wallets {
box-sizing: border-box;
padding: 60px;
overflow-y: auto;
height: calc(100vh - 60px);
&__title {
font-family: 'font_bold', sans-serif;
font-size: 32px;
color: var(--c-title);
margin-bottom: 36px;
}
&__content-area {
display: flex;
align-items: flex-start;
justify-content: space-between;
width: 100%;
min-height: 80%;
}
&__left-area {
width: 75%;
margin-right: 24px;
}
&__right-area {
width: 25%;
}
&__information {
font-size: 14px;
color: var(--c-title);
&__title {
font-family: 'font_bold', sans-serif;
font-size: 16px;
margin-bottom: 8px;
}
&__description {
font-family: 'font_regular', sans-serif;
margin-bottom: 16px;
}
&__link {
text-decoration: none;
color: var(--c-primary);
}
}
&__pagination {
width: 100%;
display: flex;
align-items: flex-end;
justify-content: space-between;
&__info {
font-family: 'font_semiBold', sans-serif;
font-size: 16px;
color: #74777e;
strong {
color: var(--c-title);
}
}
}
}
.info-block {
padding: 20px;
&.information {
background: #f8f8f9;
}
}
</style>

View File

@ -0,0 +1,15 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div id="wallets-root">
<router-view/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class WalletsRoot extends Vue {}
</script>

View File

@ -26,6 +26,7 @@
--c-button-disabled: #dadde5; --c-button-disabled: #dadde5;
--c-button-gray: #e7e9eb; --c-button-gray: #e7e9eb;
--c-payout-period: #444c63; --c-payout-period: #444c63;
--wallet-feature-opted-in: #00ce7d;
// borders // borders
--b-button-white: 1px solid #afb7c1; --b-button-white: 1px solid #afb7c1;

View File

@ -1,7 +1,6 @@
// Copyright (C) 2021 Storj Labs, Inc. // Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information. // See LICENSE for copying information.
import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import PayoutsByNodeTable from '@/app/components/payouts/tables/payoutsByNode/PayoutsByNodeTable.vue'; import PayoutsByNodeTable from '@/app/components/payouts/tables/payoutsByNode/PayoutsByNodeTable.vue';

View File

@ -4,7 +4,7 @@
import Vuex from 'vuex'; import Vuex from 'vuex';
import { Currency } from '@/app/utils/currency'; import { Currency } from '@/app/utils/currency';
import PayoutsByNode from '@/app/views/PayoutsByNode.vue'; import PayoutsByNode from '@/app/views/payouts/PayoutsByNode.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import store from '../../mock/store'; import store from '../../mock/store';

View File

@ -3,7 +3,7 @@
import Vuex from 'vuex'; import Vuex from 'vuex';
import PayoutsPage from '@/app/views/PayoutsPage.vue'; import PayoutsPage from '@/app/views/payouts/PayoutsPage.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import store from '../../mock/store'; import store from '../../mock/store';