web/satellite: update Coupons area in Billing page
The Coupons area of the Billing page has been updated to match our designs. Resolves storj/storj-private#172 Change-Id: I0f3d7f3818f47a1bbc6a2dba930ab429f83f92c2
This commit is contained in:
parent
73ff35f160
commit
c24341bcab
@ -298,6 +298,7 @@ export class PaymentsHttpApi implements PaymentsApi {
|
||||
new Date(coupon.addedAt),
|
||||
coupon.expiresAt ? new Date(coupon.expiresAt) : null,
|
||||
coupon.duration,
|
||||
coupon.partnered,
|
||||
);
|
||||
}
|
||||
|
||||
@ -328,6 +329,7 @@ export class PaymentsHttpApi implements PaymentsApi {
|
||||
new Date(coupon.addedAt),
|
||||
coupon.expiresAt ? new Date(coupon.expiresAt) : null,
|
||||
coupon.duration,
|
||||
coupon.partnered,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -3,44 +3,25 @@
|
||||
|
||||
<template>
|
||||
<div class="coupon-area">
|
||||
<div class="coupon-area__top-container">
|
||||
<h1 class="coupon-area__title">Coupon</h1>
|
||||
<VLoader v-if="isCouponFetching" />
|
||||
<div class="coupon-area__container">
|
||||
<div
|
||||
v-if="coupon"
|
||||
class="coupon-area__container__existing-coupons"
|
||||
>
|
||||
<div class="coupon-area__container__existing-coupons__discount-top-container ">
|
||||
<span :class="`coupon-area__container__existing-coupons__discount-top-container__discount ${status}-discount`">
|
||||
{{ coupon.getDescription().slice(0, coupon.getDescription().indexOf(' ')) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="coupon-area__container__existing-coupons__status-container">
|
||||
<span :class="`coupon-area__container__existing-coupons__status-container__status ${status}-status`">
|
||||
{{ status }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="coupon-area__container__existing-coupons__discount-black-container">
|
||||
<span class="coupon-area__container__existing-coupons__discount-black-container__discount">
|
||||
{{ coupon.getDescription().slice(0, coupon.getDescription().indexOf(' ')) }} off
|
||||
</span>
|
||||
</div>
|
||||
<div class="coupon-area__container__existing-coupons__expiration-container">
|
||||
<span class="coupon-area__container__existing-coupons__expiration-container__expiration">
|
||||
{{ expirationHelper }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="coupon-area__container__new-coupon"
|
||||
@click="toggleCreateModal"
|
||||
>
|
||||
<div class="coupon-area__container__new-coupon__text-area">
|
||||
<span class="coupon-area__container__new-coupon__text-area__plus-icon">+ </span>
|
||||
<span class="coupon-area__container__new-coupon__text-area__text">Apply New Coupon</span>
|
||||
</div>
|
||||
<h1 class="coupon-area__title">Coupons</h1>
|
||||
<VLoader v-if="isCouponFetching" />
|
||||
<div class="coupon-area__wrapper">
|
||||
<div v-if="coupon" class="coupon-area__wrapper__coupon">
|
||||
<div class="coupon-area__wrapper__coupon__icon" :class="{'expired': !isActive}">
|
||||
<CloudIcon v-if="coupon.partnered" />
|
||||
<CouponIcon v-else />
|
||||
</div>
|
||||
<h2 class="coupon-area__wrapper__coupon__title">{{ coupon.name }}</h2>
|
||||
<p class="coupon-area__wrapper__coupon__price">{{ discount }}</p>
|
||||
<p class="coupon-area__wrapper__coupon__status" :class="{'expired': !isActive}">{{ isActive ? 'Active' : 'Expired' }}</p>
|
||||
<p class="coupon-area__wrapper__coupon__expiration">{{ expiration }}</p>
|
||||
</div>
|
||||
<div
|
||||
class="coupon-area__wrapper__add-coupon"
|
||||
@click="toggleCreateModal"
|
||||
>
|
||||
<span class="coupon-area__wrapper__add-coupon__plus-icon">+ </span>
|
||||
<span class="coupon-area__wrapper__add-coupon__text">Apply New Coupon</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -50,15 +31,19 @@
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
|
||||
import { Coupon } from '@/types/payments';
|
||||
import { Coupon, CouponDuration } from '@/types/payments';
|
||||
import { AnalyticsHttpApi } from '@/api/analytics';
|
||||
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
|
||||
import { MODALS } from '@/utils/constants/appStatePopUps';
|
||||
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
|
||||
import { useNotify, useStore } from '@/utils/hooks';
|
||||
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
|
||||
|
||||
import VLoader from '@/components/common/VLoader.vue';
|
||||
|
||||
import CouponIcon from '@/../static/images/billing/coupon.svg';
|
||||
import CloudIcon from '@/../static/images/onboardingTour/cloudIcon.svg';
|
||||
|
||||
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
|
||||
|
||||
const store = useStore();
|
||||
@ -77,49 +62,36 @@ const coupon = computed((): Coupon | null => {
|
||||
* Returns the expiration date of the coupon.
|
||||
*/
|
||||
const expiration = computed((): string => {
|
||||
if (!coupon.value) {
|
||||
return '';
|
||||
}
|
||||
const c = coupon.value;
|
||||
if (!c) return '';
|
||||
|
||||
if (coupon.value?.expiresAt) {
|
||||
return 'Expires ' + coupon.value?.expiresAt.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
||||
} else {
|
||||
return 'Unknown expiration';
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the whether the coupon is active or not.
|
||||
*/
|
||||
const status = computed((): string => {
|
||||
if (!coupon.value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
if ((coupon.value.duration === 'forever' || coupon.value.duration === 'once') || (coupon.value.expiresAt && today.getTime() < coupon.value.expiresAt.getTime())) {
|
||||
return 'active';
|
||||
} else {
|
||||
return 'inactive';
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the whether the coupon is active or not.
|
||||
*/
|
||||
const expirationHelper = computed((): string => {
|
||||
if (!coupon.value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (coupon.value.duration) {
|
||||
case 'once':
|
||||
return 'Expires after first use';
|
||||
case 'forever':
|
||||
const exp = c.expiresAt;
|
||||
if (!exp || c.duration === CouponDuration.Forever) {
|
||||
return 'No expiration';
|
||||
default:
|
||||
return expiration.value;
|
||||
}
|
||||
return `Expires on ${exp.getDate()} ${SHORT_MONTHS_NAMES[exp.getMonth()]} ${exp.getFullYear()}`;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the coupon's discount amount.
|
||||
*/
|
||||
const discount = computed((): string => {
|
||||
const c = coupon.value;
|
||||
if (!c) return '';
|
||||
|
||||
if (c.percentOff !== 0) {
|
||||
return `${parseFloat(c.percentOff.toFixed(2)).toString()}% off`;
|
||||
}
|
||||
return `$${(c.amountOff / 100).toFixed(2).replace('.00', '')} off`;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the whether the coupon is active.
|
||||
*/
|
||||
const isActive = computed((): boolean => {
|
||||
const now = Date.now();
|
||||
const c = coupon.value;
|
||||
return !!c && (c.duration === 'forever' || (!!c.expiresAt && now < c.expiresAt.getTime()));
|
||||
});
|
||||
|
||||
/**
|
||||
@ -146,169 +118,124 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.active-discount {
|
||||
background: var(--c-green-1);
|
||||
color: var(--c-green-5);
|
||||
}
|
||||
|
||||
.inactive-discount {
|
||||
background: #ffe1df;
|
||||
color: #ac1a00;
|
||||
}
|
||||
|
||||
.active-status {
|
||||
background: var(--c-green-5);
|
||||
}
|
||||
|
||||
.inactive-status {
|
||||
background: #ac1a00;
|
||||
}
|
||||
|
||||
.coupon-area {
|
||||
margin-top: 16px;
|
||||
|
||||
&__title {
|
||||
font-family: sans-serif;
|
||||
font-size: 24px;
|
||||
margin: 20px 0;
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 18px;
|
||||
line-height: 27px;
|
||||
}
|
||||
|
||||
&__container {
|
||||
&__wrapper {
|
||||
margin-top: 19px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 16px;
|
||||
|
||||
&__existing-coupons {
|
||||
border-radius: 10px;
|
||||
max-width: 400px;
|
||||
width: 18vw;
|
||||
min-width: 227px;
|
||||
max-height: 222px;
|
||||
height: 10vw;
|
||||
min-height: 126px;
|
||||
display: grid;
|
||||
grid-template-columns: 4fr 1fr;
|
||||
grid-template-rows: 2fr 1fr 1fr;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 20px rgb(0 0 0 / 4%);
|
||||
background: #fff;
|
||||
|
||||
&__discount-top-container {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
margin: 0 0 auto;
|
||||
|
||||
&__discount {
|
||||
height: 60px;
|
||||
width: fit-content;
|
||||
min-width: 50px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
font-family: sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__status-container {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
margin: 0 0 0 auto;
|
||||
|
||||
&__status {
|
||||
height: 30px;
|
||||
width: 50px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
font-family: sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
&__discount-black-container {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
|
||||
&__discount {
|
||||
font-family: sans-serif;
|
||||
font-weight: 1000;
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
&__expiration-container {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
|
||||
&__expiration {
|
||||
font-family: sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__new-coupon {
|
||||
border: 2px dashed var(--c-grey-5);
|
||||
border-radius: 10px;
|
||||
max-width: 400px;
|
||||
width: 18vw;
|
||||
min-width: 227px;
|
||||
max-height: 222px;
|
||||
height: 10vw;
|
||||
min-height: 126px;
|
||||
padding: 18px;
|
||||
&__coupon {
|
||||
width: 340px;
|
||||
box-sizing: border-box;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
border-radius: 20px;
|
||||
background-color: var(--c-white);
|
||||
|
||||
&__text-area {
|
||||
&__icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(0 172 38 / 10%);
|
||||
|
||||
&__plus-icon {
|
||||
color: var(--c-blue-3);
|
||||
font-family: sans-serif;
|
||||
font-size: 24px;
|
||||
:deep(path) {
|
||||
fill: var(--c-green-5);
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: var(--c-blue-3);
|
||||
font-family: sans-serif;
|
||||
font-size: 18px;
|
||||
text-decoration: underline;
|
||||
&.expired {
|
||||
background-color: rgb(255 138 0 / 10%);
|
||||
|
||||
:deep(path) {
|
||||
fill: var(--c-yellow-5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 24px;
|
||||
line-height: 31px;
|
||||
}
|
||||
|
||||
&__price {
|
||||
font-family: 'font_medium', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__status {
|
||||
box-sizing: border-box;
|
||||
padding: 4px 11px;
|
||||
background-color: var(--c-green-5);
|
||||
border-radius: 4px;
|
||||
font-family: 'font_medium', sans-serif;
|
||||
font-size: 12px;
|
||||
color: var(--c-white);
|
||||
|
||||
&.expired {
|
||||
background-color: var(--c-yellow-5);
|
||||
}
|
||||
}
|
||||
|
||||
&__expiration {
|
||||
font-family: 'font_regular', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--c-grey-6);
|
||||
}
|
||||
}
|
||||
|
||||
&__add-coupon {
|
||||
width: 340px;
|
||||
min-height: 213px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px dashed var(--c-grey-5);
|
||||
border-radius: 20px;
|
||||
color: var(--c-blue-3);
|
||||
font-family: 'font_regular', sans-serif;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--c-blue-1);
|
||||
}
|
||||
|
||||
&__plus-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 18px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
|
||||
.coupon-area__container {
|
||||
.coupon-area__wrapper {
|
||||
flex-direction: column;
|
||||
|
||||
&__existing-coupons {
|
||||
max-width: unset;
|
||||
width: 90%;
|
||||
|
||||
&__discount-black-container {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&__new-coupon {
|
||||
max-width: 100%;
|
||||
width: 90%;
|
||||
&__coupon,
|
||||
&__add-coupon {
|
||||
width: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -349,25 +349,8 @@ export class Coupon {
|
||||
public addedAt: Date = new Date(),
|
||||
public expiresAt: Date | null = new Date(),
|
||||
public duration: CouponDuration = CouponDuration.Once,
|
||||
public partnered: boolean = false,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* getDescription returns the amount and duration of the coupon.
|
||||
*/
|
||||
public getDescription(): string {
|
||||
let amtOff = `${parseFloat(this.percentOff.toFixed(2)).toString()}%`;
|
||||
if (this.amountOff !== 0 || this.percentOff === 0) {
|
||||
amtOff = `$${(this.amountOff / 100).toFixed(2).replace('.00', '')}`;
|
||||
}
|
||||
let dur = this.duration.toString();
|
||||
if (this.duration === CouponDuration.Repeating && this.expiresAt !== null) {
|
||||
const months = this.expiresAt.getUTCMonth() - this.addedAt.getUTCMonth() +
|
||||
(this.expiresAt.getUTCFullYear() - this.addedAt.getFullYear()) * 12;
|
||||
dur = `for ${months} month${months !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
return `${amtOff} off ${dur}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
3
web/satellite/static/images/billing/coupon.svg
Normal file
3
web/satellite/static/images/billing/coupon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" fill="none" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m5 4.1992188c-0.7422926 0-1.4555826 0.2954124-1.9804688 0.8203124-0.5248999 0.5248863-0.8203124 1.2381762-0.8203124 1.9804688v3c-4.321e-4 0.442438 0.3583431 0.801213 0.8007812 0.800781 0.318576 0 0.6224104 0.126298 0.8476562 0.351563 0.2252911 0.225302 0.3515625 0.529132 0.3515626 0.847656 0 0.318524-0.1262715 0.622354-0.3515626 0.847656-0.2252457 0.225265-0.5290802 0.351563-0.8476562 0.351563-0.4424381-4.32e-4 -0.8012133 0.358343-0.8007812 0.800781v3c0 0.742275 0.2954438 1.455572 0.8203124 1.980469 0.5248928 0.524935 1.2381863 0.820312 1.9804688 0.820312h14c0.742283 0 1.455576-0.295377 1.980469-0.820312 0.524868-0.524897 0.820312-1.238194 0.820312-1.980469v-3c4.32e-4 -0.442438-0.358343-0.801213-0.800781-0.800781-0.318576 0-0.62241-0.126298-0.847656-0.351563-0.225291-0.225302-0.351563-0.529132-0.351563-0.847656s0.126272-0.622354 0.351563-0.847656c0.225246-0.225265 0.52908-0.351563 0.847656-0.351563 0.442438 4.32e-4 0.801213-0.358343 0.800781-0.800781v-3c0-0.7422926-0.295412-1.4555824-0.820312-1.9804688-0.524886-0.5249-1.238176-0.8203124-1.980469-0.8203124zm0 1.6015624h9.199219v1.1992188c-4.32e-4 0.4424381 0.358343 0.8012133 0.800781 0.8007812 0.442438 4.321e-4 0.801213-0.3583431 0.800781-0.8007812v-1.1992188h3.199219c0.318566 2e-7 0.622403 0.1263034 0.847656 0.3515626 0.225259 0.2252532 0.351563 0.5290899 0.351563 0.8476562v2.3164062c-0.44158 0.1318308-0.848224 0.3716358-1.179688 0.7031248-0.524868 0.524897-0.820312 1.238194-0.820312 1.980469s0.295444 1.455572 0.820312 1.980469c0.331464 0.33149 0.738108 0.571294 1.179688 0.703125v2.316406c0 0.318524-0.126272 0.622354-0.351563 0.847656-0.225246 0.225265-0.52908 0.351563-0.847656 0.351563h-3.199219v-1.199219c4.32e-4 -0.442438-0.358343-0.801213-0.800781-0.800781-0.442438-4.32e-4 -0.801213 0.358343-0.800781 0.800781v1.199219h-9.199219c-0.3185764 0-0.6224097-0.126298-0.8476562-0.351563-0.2252907-0.225302-0.3515625-0.529132-0.3515626-0.847656v-2.316406c0.4415798-0.131831 0.848224-0.371635 1.1796876-0.703125 0.5248679-0.524897 0.8203125-1.238194 0.8203124-1.980469 0-0.742275-0.2954445-1.455572-0.8203124-1.980469-0.3314636-0.331489-0.7381079-0.5712941-1.1796876-0.7031248v-2.3164062c0-0.3185663 0.1263034-0.622403 0.3515626-0.8476562 0.2252532-0.2252592 0.5290899-0.3515624 0.8476562-0.3515626zm10 4.3984378c-0.442438-4.32e-4 -0.801213 0.358343-0.800781 0.800781v2c-4.32e-4 0.442438 0.358343 0.801213 0.800781 0.800781 0.442438 4.32e-4 0.801213-0.358343 0.800781-0.800781v-2c4.32e-4 -0.442438-0.358343-0.801213-0.800781-0.800781z" fill="#000" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
Loading…
Reference in New Issue
Block a user