web/satellite: referral links (#3655)

This commit is contained in:
Nikolay Yurchenko 2019-11-26 18:54:42 +02:00 committed by GitHub
parent 854e5507ab
commit cf13cf6e00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 330 additions and 5 deletions

View File

@ -0,0 +1,38 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { HttpClient } from '@/utils/httpClient';
/**
* ReferralHttpApi is a console Referral API.
* Exposes all referral-related functionality
*/
export class ReferralHttpApi {
private readonly http: HttpClient = new HttpClient();
private readonly ROOT_PATH: string = '/api/v0/referrals';
/**
* Used to get referral links
*
* @throws Error
*/
public async getLinks(): Promise<any> {
const path = `${this.ROOT_PATH}`;
const response = await this.http.get(path, true);
// TODO: remove mock and add types after final referral manager implementation
return [];
if (response.ok) {
return await response.json();
}
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not get referral links');
}
}

View File

@ -0,0 +1,166 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="referral-container">
<div class="referral-container__title-container">
<p class="referral-container__title-container__text">Refer A Friend And Help Build The</p>
<p class="referral-container__title-container__text">Decentralized Future</p>
</div>
<div class="referral-container__available" v-if="isAvailableLinks">
<p class="referral-container__available__title">You Have {{ 5 }} Invitations To Share!</p>
<div class="referral-container__copy-and-share-container__link-holder">
<p class="referral-container__copy-and-share-container__link-holder__link">https://us-central-1.tardigrade.io/ref/?uuid=96a33796-2c9b-47</p>
<div class="copy-button" v-clipboard="'test'" @click="copyLink">Copy</div>
</div>
<div class="referral-container__copy-and-share-container__link-holder">
<p class="referral-container__copy-and-share-container__link-holder__link">https://us-central-1.tardigrade.io/ref/?uuid=96a33796-2c9b-47</p>
<div class="copy-button" v-clipboard="'test'" @click="copyLink">Copy</div>
</div>
<div class="referral-container__copy-and-share-container__link-holder">
<p class="referral-container__copy-and-share-container__link-holder__link">https://us-central-1.tardigrade.io/ref/?uuid=96a33796-2c9b-47</p>
<div class="copy-button" v-clipboard="'test'" @click="copyLink">Copy</div>
</div>
<div class="referral-container__copy-and-share-container__link-holder">
<p class="referral-container__copy-and-share-container__link-holder__link">https://us-central-1.tardigrade.io/ref/?uuid=96a33796-2c9b-47</p>
<div class="copy-button" v-clipboard="'test'" @click="copyLink">Copy</div>
</div>
<div class="referral-container__copy-and-share-container__link-holder">
<p class="referral-container__copy-and-share-container__link-holder__link">https://us-central-1.tardigrade.io/ref/?uuid=96a33796-2c9b-47</p>
<div class="copy-button" v-clipboard="'test'" @click="copyLink">Copy</div>
</div>
</div>
<div class="referral-container__not-available" v-if="!isAvailableLinks">
<p class="referral-container__not-available__text">No available referral links. Try again later.</p>
<NoLinksIcon />
</div>
</div>
</template>
<script lang="ts">
import VueClipboards from 'vue-clipboards';
import { Component, Vue } from 'vue-property-decorator';
import NoLinksIcon from '@/../static/images/referral/NoLinks.svg';
import { REFERRAL_ACTIONS } from '@/store/modules/referral';
Vue.use(VueClipboards);
@Component({
components: {
NoLinksIcon,
},
})
export default class ReferralArea extends Vue {
public copyLink(): void {
this.$notify.success('Link saved to clipboard');
}
public get isAvailableLinks(): boolean {
return this.$store.state.referralModule.referralLinks.length !== 0;
}
public async beforeMount() {
await this.$store.dispatch(REFERRAL_ACTIONS.GET_LINKS);
}
}
</script>
<style scoped lang="scss">
::-webkit-scrollbar {
width: 0;
}
.referral-container {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
&__title-container {
display: flex;
flex-direction: column;
justify-items: center;
align-items: center;
&__text {
text-align: center;
font-family: 'font_bold', sans-serif;
font-size: 32px;
line-height: 40px;
color: #384b65;
margin: 0;
}
}
&__available {
margin-top: 60px;
padding-bottom: 75px;
&__title {
text-align: center;
font-family: 'font_medium', sans-serif;
font-size: 26px;
color: #354049;
}
}
&__copy-and-share-container {
background-color: #fff;
margin-top: 40px;
padding: 40px 136px 40px 136px;
border-radius: 24px;
&__link-holder {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px 10px 21px;
background-color: white;
border-radius: 6px;
margin-top: 24px;
&__link {
font-size: 16px;
line-height: 134%;
color: #494949;
margin-right: 10px;
}
.copy-button {
width: 140px;
height: 46px;
display: flex;
align-items: center;
justify-content: center;
background-color: #2683ff;
color: #fff;
font-family: 'font_regular', sans-serif;
border-radius: 6px;
font-weight: 900;
font-size: 16px;
line-height: 23px;
&:hover {
box-shadow: 0 4px 14px rgba(38, 131, 255, 0.3);
cursor: pointer;
}
}
}
}
&__not-available {
&__text {
margin: 30px 0 60px 0;
text-align: center;
font-family: 'font_medium', sans-serif;
font-size: 16px;
color: #384b65;
}
}
}
</style>

View File

@ -4,7 +4,7 @@
<template>
<div class="empty-state">
<div class="empty-state__wrap">
<h1 class="empty-state__wrap__title">{{mainTitle}}</h1>
<p class="empty-state__wrap__title">{{mainTitle}}</p>
<div class="empty-state__wrap__additional-text" v-html="additionalText"></div>
<div v-if="isButtonShown">
<VButton

View File

@ -77,6 +77,7 @@ export default class NavigationArea extends Vue {
public readonly accountNavigation: NavigationLink[] = [
RouteConfig.Account.with(RouteConfig.Profile),
RouteConfig.Account.with(RouteConfig.Billing),
RouteConfig.Account.with(RouteConfig.Referral),
];
public onLogoClick(): void {

View File

@ -8,6 +8,7 @@ import AccountArea from '@/components/account/AccountArea.vue';
import AccountBilling from '@/components/account/billing/BillingArea.vue';
import BillingHistory from '@/components/account/billing/billingHistory/BillingHistory.vue';
import ProfileArea from '@/components/account/ProfileArea.vue';
import ReferralArea from '@/components/account/referral/ReferralArea.vue';
import ApiKeysArea from '@/components/apiKeys/ApiKeysArea.vue';
import BucketArea from '@/components/buckets/BucketArea.vue';
import Page404 from '@/components/errors/Page404.vue';
@ -47,6 +48,7 @@ export abstract class RouteConfig {
public static Profile = new NavigationLink('profile', 'Profile');
public static Billing = new NavigationLink('billing', 'Billing');
public static BillingHistory = new NavigationLink('billing-history', 'Billing History');
public static Referral = new NavigationLink('referral', 'Referral');
// not in project yet
// public static Referral = new NavigationLink('//ref/:ids', 'Referral');
@ -105,6 +107,11 @@ export const router = new Router({
name: RouteConfig.BillingHistory.name,
component: BillingHistory,
},
{
path: RouteConfig.Referral.path,
name: RouteConfig.Referral.name,
component: ReferralArea,
},
],
},
{

View File

@ -11,6 +11,7 @@ import { CreditsApiGql } from '@/api/credits';
import { PaymentsHttpApi } from '@/api/payments';
import { ProjectMembersApiGql } from '@/api/projectMembers';
import { ProjectsApiGql } from '@/api/projects';
import { ReferralHttpApi } from '@/api/referral';
import { ProjectUsageApiGql } from '@/api/usage';
import { notProjectRelatedRoutes, router } from '@/router';
import { ApiKeysState, makeApiKeysModule } from '@/store/modules/apiKeys';
@ -21,6 +22,7 @@ import { makeNotificationsModule, NotificationsState } from '@/store/modules/not
import { makePaymentsModule, PaymentsState } from '@/store/modules/payments';
import { makeProjectMembersModule, ProjectMembersState } from '@/store/modules/projectMembers';
import { makeProjectsModule, PROJECTS_MUTATIONS, ProjectsState } from '@/store/modules/projects';
import { makeReferralModule, ReferralState } from '@/store/modules/referral';
import { makeUsageModule, UsageState } from '@/store/modules/usage';
import { makeUsersModule, USER_ACTIONS } from '@/store/modules/users';
import { CreditUsage } from '@/types/credits';
@ -44,6 +46,7 @@ const projectMembersApi = new ProjectMembersApiGql();
const projectsApi = new ProjectsApiGql();
const projectUsageApi = new ProjectUsageApiGql();
const paymentsApi = new PaymentsHttpApi();
const referralApi = new ReferralHttpApi();
class ModulesState {
public notificationsModule: NotificationsState;
@ -55,6 +58,7 @@ class ModulesState {
public usersModule: User;
public projectsModule: ProjectsState;
public usageModule: UsageState;
public referralModule: ReferralState;
}
// Satellite store (vuex)
@ -70,6 +74,7 @@ export const store = new Vuex.Store<ModulesState>({
projectsModule: makeProjectsModule(projectsApi),
usageModule: makeUsageModule(projectUsageApi),
bucketUsageModule: makeBucketsModule(bucketsApi),
referralModule: makeReferralModule(referralApi),
},
});

View File

@ -0,0 +1,50 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { StoreModule } from '@/store';
import { ReferralApi } from '@/types/referral';
export const REFERRAL_ACTIONS = {
GET_LINKS: 'getReferralLinks',
};
export const REFERRAL_MUTATIONS = {
SET_LINKS: 'setReferralLinks',
};
const {
GET_LINKS,
} = REFERRAL_ACTIONS;
const {
SET_LINKS,
} = REFERRAL_MUTATIONS;
export class ReferralState {
public referralLinks = [];
}
/**
* creates referral module with all dependencies
*
* @param api - referral api
*/
export function makeReferralModule(api: ReferralApi): StoreModule<ReferralState> {
return {
state: new ReferralState(),
mutations: {
[SET_LINKS](state: ReferralState, referralLinks): void {
state.referralLinks = referralLinks;
},
},
actions: {
[GET_LINKS]: async function ({commit}: any): Promise<any> {
const referralLinks = await api.getLinks();
commit(GET_LINKS, referralLinks);
return referralLinks;
},
},
};
}

View File

@ -0,0 +1,15 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
/**
* Exposes all referral-related functionality
*/
export interface ReferralApi {
/**
* Get referral links for account
*
* @returns links
* @throws Error
*/
getLinks(): Promise<any>;
}

View File

@ -0,0 +1,32 @@
<svg width="307" height="286" viewBox="0 0 307 286" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M158.1 284.9C236.6 284.9 300.2 221.1 300.2 142.4C300.2 63.7 236.6 0 158.1 0C79.6 0 16 63.8 16 142.5C16 221.2 79.6 284.9 158.1 284.9Z" fill="#E8EAF2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.0277 212.52C57.8968 212.52 60.2878 210.129 60.2878 207.26C60.2878 204.391 57.8968 202 55.0277 202C52.1585 202 49.7676 204.391 49.7676 207.26C49.7676 210.209 52.0788 212.52 55.0277 212.52Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.1722 212.52C73.0413 212.52 75.4323 210.129 75.4323 207.26C75.4323 204.391 73.0413 202 70.1722 202C67.303 202 64.9121 204.391 64.9121 207.26C64.8324 210.209 67.2233 212.52 70.1722 212.52Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M85.2347 212.52C88.1039 212.52 90.4948 210.129 90.4948 207.26C90.4948 204.391 88.1039 202 85.2347 202C82.3656 202 79.9746 204.391 79.9746 207.26C79.9746 210.209 82.3656 212.52 85.2347 212.52Z" fill="#8F96AD"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M303.683 187C305.593 187 307 188.508 307 190.417C306.095 216.347 295.14 239.764 277.953 256.95C260.767 274.136 237.348 285.091 211.417 285.995C209.508 286.096 208 284.588 208 282.679V253.231C208 251.422 209.407 250.015 211.216 249.915C227.297 249.111 241.771 242.176 252.525 231.523C263.279 220.769 270.114 206.297 270.918 190.216C271.018 188.407 272.425 187 274.235 187H303.683Z" fill="#B0B6C9"/>
<path d="M144.245 268.276C149.351 268.276 153.49 264.137 153.49 259.031C153.49 253.925 149.351 249.786 144.245 249.786C139.139 249.786 135 253.925 135 259.031C135 264.137 139.139 268.276 144.245 268.276Z" fill="#8F96AD"/>
<path d="M223.301 39.5H218.401C215.901 39.5 213.801 37.5 213.801 35.1C213.801 32.7 215.901 30.7 218.401 30.7H223.301C225.801 30.7 227.901 32.7 227.901 35.1C227.901 37.5 225.801 39.5 223.301 39.5Z" fill="white"/>
<path d="M202.5 21.8H157.6C155.1 21.8 153 19.8 153 17.4C153 15 155.1 13 157.6 13H202.5C205 13 207.1 15 207.1 17.4C207.1 19.8 205 21.8 202.5 21.8Z" fill="#F5F6FA"/>
<path d="M194.143 217.883H121.067C115.095 217.883 110.209 213.008 110.209 207.051V60.9226C110.209 54.9649 115.095 50.0903 121.067 50.0903H194.143C200.115 50.0903 205.001 54.9649 205.001 60.9226V207.051C205.001 213.117 200.115 217.883 194.143 217.883Z" fill="#B0B6C9"/>
<path d="M194.143 104.568H121.067C115.095 104.568 110.209 99.6939 110.209 93.7361V60.9226C110.209 54.9649 115.095 50.0903 121.067 50.0903H194.143C200.115 50.0903 205.001 54.9649 205.001 60.9226V93.7361C205.001 99.8022 200.115 104.568 194.143 104.568Z" fill="#9CA4BD"/>
<path d="M194.371 218.21H121.379C115.197 218.21 110.1 213.193 110.1 207.108V186.612H204.891V207.855C204.891 213.62 200.119 218.21 194.371 218.21Z" fill="#BDC3D5"/>
<path d="M158.477 207.532C161.486 207.532 163.925 205.093 163.925 202.084C163.925 199.075 161.486 196.636 158.477 196.636C155.468 196.636 153.029 199.075 153.029 202.084C153.029 205.093 155.468 207.532 158.477 207.532Z" fill="#444E59"/>
<path d="M131.674 117.643H185.389" stroke="#8F96AD" stroke-width="4.35825" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M142.35 134.531H176.126" stroke="#8F96AD" stroke-width="4.35825" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M131.674 149.894H185.389" stroke="#8F96AD" stroke-width="4.35825" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M158.04 89.3145C164.058 89.3145 168.936 84.1925 168.936 77.8741C168.936 71.5557 164.058 66.4337 158.04 66.4337C152.023 66.4337 147.145 71.5557 147.145 77.8741C147.145 84.1925 152.023 89.3145 158.04 89.3145Z" fill="#EDF1F9"/>
<path d="M176.128 166.782C176.128 170.16 173.404 172.884 170.027 172.884H147.037C143.659 172.884 140.936 170.16 140.936 166.782C140.936 163.405 143.659 160.681 147.037 160.681H170.027C173.404 160.572 176.128 163.296 176.128 166.782Z" fill="#8F96AD"/>
<g clip-path="url(#clip0)">
<path d="M150.42 64.1158C143.123 68.3245 140.619 77.6528 144.828 84.95C149.036 92.2472 158.365 94.752 165.662 90.5432C172.96 86.3344 175.464 77.0058 171.255 69.7084C167.046 62.411 157.718 59.907 150.42 64.1158ZM158.175 80.5123L156.225 80.5534L155.342 81.0624L155.342 81.0621L156.257 84.7049L153.933 86.0454L153.018 82.4026L152.932 82.452C151.515 83.2693 149.704 82.7833 148.887 81.3658C148.069 79.9488 148.556 78.1377 149.973 77.3205L153.265 75.4216L154.277 73.7547L158.175 80.5123ZM161.963 80.4318L159.979 80.4739L155.269 72.3073L155.095 72.408L156.243 70.5157C156.859 70.1604 157.647 70.3718 158.002 70.988L162.435 78.6728L162.435 78.6727C162.79 79.2889 162.579 80.0764 161.963 80.4318ZM163.528 76.1767C163.611 75.3679 163.454 74.5273 163.018 73.771C162.582 73.0147 161.933 72.4576 161.191 72.1245L161.529 70.8666C162.589 71.2936 163.519 72.0633 164.133 73.1277C164.747 74.1921 164.947 75.3829 164.786 76.5144L163.528 76.1767ZM167.025 77.1153L165.777 76.7804C166.002 75.3947 165.771 73.9262 165.017 72.6181C164.262 71.3101 163.107 70.3744 161.795 69.8751L162.13 68.6275C163.761 69.2157 165.201 70.3587 166.133 71.9744C167.065 73.5902 167.333 75.4094 167.025 77.1153Z" fill="#363840"/>
</g>
<path d="M249.8 21.8H242.8C240.2 21.8 238.1 19.8 238.1 17.4C238.1 15 240.2 13 242.8 13H249.8C252.4 13 254.5 15 254.5 17.4C254.5 19.8 252.4 21.8 249.8 21.8Z" fill="#363840"/>
<path d="M41.2664 271.3C49.5664 269.3 54.6664 260.7 52.6664 252.1C50.6664 243.5 42.1664 238.2 33.8664 240.1C25.5664 242 20.3664 250.8 22.4664 259.3C24.3664 268 32.8664 273.3 41.2664 271.3Z" fill="#8F96AD"/>
<path d="M39.1657 262.5C42.8657 261.6 45.2657 257.9 44.3657 254.1C43.4657 250.4 39.7657 248 35.9657 248.9C32.2657 249.8 29.8657 253.5 30.7657 257.3C31.5657 261.2 35.3657 263.4 39.1657 262.5Z" fill="#F5F6FA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M291.502 193C297.228 193 302 188.227 302 182.5C302 176.773 297.228 172 291.502 172C285.776 172 281.004 176.773 281.004 182.5C280.845 188.227 285.617 193 291.502 193Z" fill="#7A83A0"/>
<path d="M110.6 54C110.2 54 109.9 54 109.6 54C109 54 108.4 53.7 108.1 53.2C100.9 40 86.7 31 70.4 31C55.7 31 42.7 38.4 35 49.6C34.6 50.2 33.9 50.4 33.3 50.3C31.4 49.9 29.4 49.7 27.3 49.7C12.2 49.6 0 61.7 0 76.7C0 77.6 0.7 78.3 1.6 78.3H131.8C132.7 78.3 133.4 77.6 133.4 76.7C133.4 64.2 123.2 54 110.6 54Z" fill="#CBD0DF"/>
<defs>
<clipPath id="clip0">
<rect width="30.5077" height="30.5077" fill="white" transform="translate(142.787 62.0755)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -79,6 +79,11 @@ exports[`NavigationArea snapshot not changed with project 1`] = `
<h1 class="navigation-area__item-container__link-container__title account-item">Billing</h1>
</div>
</router-link-stub>
<router-link-stub to="/account/referral" tag="a" event="click" class="navigation-area__item-container account-item">
<div class="navigation-area__item-container__link-container">
<h1 class="navigation-area__item-container__link-container__title account-item">Referral</h1>
</div>
</router-link-stub>
</div>
<div class="divider custom-divider"></div>
</div>
@ -164,6 +169,11 @@ exports[`NavigationArea snapshot not changed without project 1`] = `
<h1 class="navigation-area__item-container__link-container__title account-item">Billing</h1>
</div>
</router-link-stub>
<router-link-stub to="/account/referral" tag="a" event="click" class="navigation-area__item-container account-item">
<div class="navigation-area__item-container__link-container">
<h1 class="navigation-area__item-container__link-container__title account-item">Referral</h1>
</div>
</router-link-stub>
</div>
<div class="divider custom-divider"></div>
</div>

View File

@ -41,7 +41,7 @@ describe('NavigationArea', () => {
const resourcesButton = wrapper.findAll('.navigation-area__resources-title__button');
const accountButton = wrapper.findAll('.navigation-area__account-title__button');
expect(navigationElements.length).toBe(8);
expect(navigationElements.length).toBe(9);
expect(disabledElements.length).toBe(4);
expect(resourcesButton.length).toBe(0);
expect(accountButton.length).toBe(0);
@ -63,7 +63,7 @@ describe('NavigationArea', () => {
const resourcesButton = wrapper.findAll('.navigation-area__resources-title__button');
const accountButton = wrapper.findAll('.navigation-area__account-title__button');
expect(navigationElements.length).toBe(8);
expect(navigationElements.length).toBe(9);
expect(disabledElements.length).toBe(0);
expect(resourcesButton.length).toBe(0);
expect(accountButton.length).toBe(0);
@ -110,7 +110,7 @@ describe('NavigationArea', () => {
wrapper.find('.navigation-area__resources-title__button').trigger('click');
wrapper.find('.navigation-area__account-title__button').trigger('click');
expect(wrapper.findAll('.navigation-area__item-container').length).toBe(8);
expect(wrapper.findAll('.navigation-area__item-container').length).toBe(9);
wrapper.find('.navigation-area__resources-title').trigger('mouseleave');
wrapper.find('.navigation-area__account-title').trigger('mouseleave');
@ -118,6 +118,6 @@ describe('NavigationArea', () => {
expect(wrapper.findAll('.navigation-area__resources-title__button').length).toBe(0);
expect(wrapper.findAll('.navigation-area__account-title__button').length).toBe(0);
expect(wrapper.findAll('.navigation-area__item-container').length).toBe(8);
expect(wrapper.findAll('.navigation-area__item-container').length).toBe(9);
});
});

View File

@ -20,6 +20,7 @@ module.exports = {
}),
new StyleLintPlugin({
files: ['**/*.{vue,sss,less,scss,sass}'],
emitWarning: true,
})
],
},