Web: Added payments page.

This commit is contained in:
Tatiana Lopaeva 2022-08-16 10:49:34 +03:00
parent b6559781ea
commit 12a83327b0
15 changed files with 607 additions and 1 deletions

View File

@ -0,0 +1,3 @@
<svg width="17" height="2" viewBox="0 0 17 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5 6.99382e-07L16.5 2L0.5 2L0.5 0L16.5 6.99382e-07Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 183 B

View File

@ -0,0 +1,3 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.5 0H7.5V7H0.5V9H7.5V16H9.5V9H16.5V7H9.5V0Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 216 B

View File

@ -0,0 +1,20 @@
{
"AddedManagers": "Managers added",
"StorageSpace": "Storage space used",
"TariffsPlans": "Tariffs plans",
"SuitablePlan": "Choose the plan that suits your team",
"PerUserMonth": "per manager/month",
"HaveQuestions": "Have questions?",
"ContactUs": "For sales questions, contact us at",
"ManagersNumber": "Number of managers",
"StartPrice": "From {{price}} per manager/month",
"RenewSubscription": "Renew subscription to {{organizationName}} Business cloud",
"ReActivate": "Re-activate business plan?",
"Benefits": "Benefits",
"BusinessTitle": "You are using Business plan",
"StartupTitle": "You are using free Startup plan",
"PriceCalculation": "Calculate your price",
"UpgradeNow": "Upgrade now",
"StartupSuggestion": "Do more with Business plan",
"BusinessSuggestion": "Customize your Business plan"
}

View File

@ -60,7 +60,7 @@ import DialogsWrapper from "./components/dialogs/DialogsWrapper";
// "/preparation-portal"
// );
const Payments = React.lazy(() => import("./pages/Payments"));
const Payments = React.lazy(() => import("./pages/TariffsPage"));
const Error404 = React.lazy(() => import("client/Error404"));
const Error401 = React.lazy(() => import("client/Error401"));
const Files = React.lazy(() => import("./pages/Files")); //import("./components/pages/Home"));

View File

@ -0,0 +1,70 @@
import React, { useState } from "react";
import styled from "styled-components";
import { useTranslation, Trans } from "react-i18next";
import Text from "@docspace/components/text";
import { inject, observer } from "mobx-react";
import { smallTablet } from "@docspace/components/utils/device";
const StyledBody = styled.div`
border-radius: 12px;
border: 1px solid #f8f9f9;
max-width: 320px;
@media ${smallTablet} {
max-width: 600px;
}
padding: 24px;
box-sizing: border-box;
background: #f8f9f9;
p {
margin-bottom: 24px;
}
.tariff-benefits_text {
margin-bottom: 20px;
}
.tariff-benefits {
display: flex;
margin-bottom: 16px;
align-items: baseline;
p {
margin-left: 8px;
}
}
`;
const BenefitsContainer = ({ t, availableTariffs }) => {
return (
<StyledBody>
<Text
fontSize={"16px"}
fontWeight={"600"}
className="tariff-benefits_text"
noSelect
>
{t("Benefits")}
</Text>
{availableTariffs.map((item, index) => {
return (
<div className="tariff-benefits" key={index}>
<Text noSelect>{"*" + item}</Text>
</div>
);
})}
</StyledBody>
);
};
export default inject(({ auth, payments }) => {
const { tariffsInfo } = payments;
const availableTariffs = [
"Unlimited number of users",
"Unlimited number of active rooms",
"100 GB per manager add space on request",
"Automatic backup & recovery",
"Tracking user logins & actions",
];
return { tariffsInfo, availableTariffs };
})(observer(BenefitsContainer));

View File

@ -0,0 +1,42 @@
import React from "react";
import styled from "styled-components";
import { withRouter } from "react-router";
import { useTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import Text from "@docspace/components/text";
import Link from "@docspace/components/link";
const StyledContactContainer = styled.div`
display: flex;
width: max-content;
p {
margin-right: 8px;
}
`;
const ContactContainer = ({ t, salesEmail, theme }) => {
return (
<StyledContactContainer>
<Text noSelect fontWeight={600}>
{t("ContactUs")}
</Text>
<Link
fontWeight={600}
href={`mailto:${salesEmail}`}
color={theme.client.payments.linkColor}
>
{salesEmail}
</Link>
</StyledContactContainer>
);
};
export default inject(({ payments, auth }) => {
const { salesEmail } = payments;
return {
salesEmail,
theme: auth.settingsStore.theme,
};
})(withRouter(observer(ContactContainer)));

View File

@ -0,0 +1,62 @@
import React from "react";
import styled from "styled-components";
import Text from "@docspace/components/text";
import { useTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
const StyledCurrentTariffContainer = styled.div`
display: flex;
height: 40px;
background: #f8f9f9;
margin-bottom: 24px;
grid-template-columns: repeat(auto, minmax(150px, auto));
white-space: nowrap;
grid-gap: 20px;
p {
padding: 12px 16px;
}
`;
const CurrentTariffContainer = ({ quota, style }) => {
const { t } = useTranslation("Payments");
const { usersCount, maxUsersCount, storageSize, usedSize } = quota;
const convertedBytes = (bytes) => {
const sizes = ["Bytes", "Kb", "Mb", "Gb", "Tb"]; //TODO: need to specify about translation
if (bytes == 0) return "0 B";
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + " " + sizes[i];
};
const storageSizeConverted = convertedBytes(storageSize);
const usedSizeConverted = convertedBytes(usedSize);
return (
<StyledCurrentTariffContainer style={style} className="current-tariff">
<Text isBold noSelect>
{t("AddedManagers")}:{" "}
<Text as="span" isBold>
{usersCount}/{maxUsersCount}
</Text>
</Text>
<Text isBold noSelect>
{t("StorageSpace")}:{" "}
<Text as="span" isBold>
{usedSizeConverted}/{storageSizeConverted}
</Text>
</Text>
</StyledCurrentTariffContainer>
);
};
export default inject(({ auth }) => {
const { quota } = auth;
return { quota };
})(observer(CurrentTariffContainer));

View File

@ -0,0 +1,69 @@
import React, { useState } from "react";
import styled from "styled-components";
import { useTranslation, Trans } from "react-i18next";
import Text from "@docspace/components/text";
import { inject, observer } from "mobx-react";
import SelectUsersCountContainer from "./sub-components/SelectUsersCountContainer";
import TotalTariffContainer from "./sub-components/TotalTariffContainer";
import { smallTablet } from "@docspace/components/utils/device";
const StyledBody = styled.div`
border-radius: 12px;
border: 1px solid #d0d5da;
max-width: 320px;
@media ${smallTablet} {
max-width: 600px;
}
padding: 24px;
box-sizing: border-box;
p {
margin-bottom: 24px;
}
`;
const step = "1",
minUsersCount = 1,
maxUsersCount = 1000;
const PriceCalculation = ({ t, price }) => {
const [usersCount, setUsersCount] = useState(minUsersCount);
const onSliderChange = (e) => {
const count = parseFloat(e.target.value);
count > minUsersCount ? setUsersCount(count) : setUsersCount(minUsersCount);
};
const onPlusClick = () => {
usersCount < maxUsersCount && setUsersCount(usersCount + 1);
};
const onMinusClick = () => {
usersCount > minUsersCount && setUsersCount(usersCount - 1);
};
return (
<StyledBody>
<Text fontSize="16px" fontWeight={600} noSelect>
{t("PriceCalculation")}
</Text>
<SelectUsersCountContainer
maxUsersCount={maxUsersCount}
step={step}
usersCount={usersCount}
onMinusClick={onMinusClick}
onPlusClick={onPlusClick}
onSliderChange={onSliderChange}
/>
<TotalTariffContainer t={t} usersCount={usersCount} price={price} />
</StyledBody>
);
};
export default inject(({ auth, payments }) => {
const { tariffsInfo } = payments;
return { tariffsInfo };
})(observer(PriceCalculation));

View File

@ -0,0 +1,127 @@
import React, { useEffect } from "react";
import styled from "styled-components";
import { withRouter } from "react-router";
import { useTranslation, Trans } from "react-i18next";
import PropTypes from "prop-types";
import Section from "@docspace/common/components/Section";
import Loader from "@docspace/components/loader";
import { setDocumentTitle } from "@docspace/client/src/helpers/filesUtils";
import { inject, observer } from "mobx-react";
import Text from "@docspace/components/text";
import CurrentTariffContainer from "./CurrentTariffContainer";
import PriceCalculation from "./PriceCalculation";
import BenefitsContainer from "./BenefitsContainer";
import { smallTablet } from "@docspace/components/utils/device";
import ContactContainer from "./ContactContainer";
const StyledBody = styled.div`
max-width: 660px;
p {
margin-bottom: 16px;
}
.payments-info_suggestion {
margin-bottom: 12px;
}
.payments-info_managers-price {
margin-bottom: 20px;
}
.payments-info {
display: grid;
grid-template-columns: repeat(2, minmax(100px, 320px));
grid-gap: 20px;
margin-bottom: 20px;
@media ${smallTablet} {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
}
`;
const TariffsPageWrapper = ({
setQuota,
quota,
setTariffsInfo,
tariffsInfo,
organizationName,
isStartup,
price,
}) => {
const { t, ready } = useTranslation("Payments");
useEffect(() => {
setDocumentTitle(t("TariffsPlans")); //TODO: need to specify
}, [ready]);
useEffect(() => {
(async () => {
try {
await Promise.all([setQuota(), setTariffsInfo()]);
} catch (error) {
toastr.error(error);
}
})();
}, []);
return (
<StyledBody>
<Text noSelect fontSize="16px" isBold>
{isStartup ? t("StartupTitle") : t("BusinessTitle")}
</Text>
<CurrentTariffContainer />
<Text
noSelect
fontSize="16px"
isBold
className="payments-info_suggestion"
>
{isStartup ? t("StartupSuggestion") : t("BusinessSuggestion")}
</Text>
<Text
noSelect
fontWeight={600}
fontSize={"14"}
className="payments-info_managers-price"
>
<Trans t={t} i18nKey="StartPrice" ns="Payments">
{{ price: price }}
</Trans>
</Text>
<div className="payments-info">
<PriceCalculation t={t} price={price} />
<BenefitsContainer t={t} />
</div>
<ContactContainer t={t} />
</StyledBody>
);
};
TariffsPageWrapper.propTypes = {
isLoaded: PropTypes.bool,
};
const TariffsPage = (props) => {
return (
<Section>
<Section.SectionBody>
<TariffsPageWrapper {...props} />
</Section.SectionBody>
</Section>
);
};
export default inject(({ auth, payments }) => {
const { setQuota, quota } = auth;
const { organizationName } = auth.settingsStore;
const { setTariffsInfo, tariffsInfo } = payments;
const isStartup = true;
const price = "30";
return {
setQuota,
quota,
organizationName,
setTariffsInfo,
tariffsInfo,
isStartup,
price,
};
})(withRouter(observer(TariffsPage)));

View File

@ -0,0 +1,92 @@
import React from "react";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import Text from "@docspace/components/text";
import Slider from "@docspace/components/slider";
import PlusIcon from "../../../../public/images/plus.react.svg";
import MinusIcon from "../../../../public/images/minus.react.svg";
import { smallTablet } from "@docspace/components/utils/device";
const StyledBody = styled.div`
max-width: 272px;
margin: 0 auto;
@media ${smallTablet} {
max-width: 520px;
}
.tariff-users {
display: flex;
align-items: center;
margin: 0 auto;
width: max-content;
.tariff-score,
.circle {
cursor: pointer;
}
.circle {
background: #f3f4f4;
display: flex;
border: 1px solid #f3f4f4;
border-radius: 50%;
width: 40px;
height: 40px;
justify-content: center;
-ms-align-items: center;
align-items: center;
}
}
.tariff-users_count {
margin-left: 20px;
margin-right: 20px;
text-align: center;
width: 102px;
}
.tariff-users_text {
margin-bottom: 12px;
text-align: center;
}
`;
const SelectUsersCountContainer = ({
maxUsersCount,
step,
usersCount,
onMinusClick,
onPlusClick,
onSliderChange,
}) => {
const { t } = useTranslation("Payments");
return (
<StyledBody>
<Text noSelect fontWeight={600} className="tariff-users_text">
{t("ManagersNumber")}
</Text>
<div className="tariff-users">
<div className="circle" onClick={onMinusClick}>
<MinusIcon onClick={onMinusClick} className="tariff-score" />
</div>
<Text noSelect fontSize={"44px"} className="tariff-users_count" isBold>
{usersCount}
</Text>
<div className="circle" onClick={onPlusClick}>
<PlusIcon onClick={onPlusClick} className="tariff-score" />
</div>
</div>
<Slider
type="range"
min={"0"}
max={maxUsersCount.toString()}
step={step}
withPouring
value={usersCount}
onChange={onSliderChange}
colorPouring={"#20D21F"}
/>
</StyledBody>
);
};
export default SelectUsersCountContainer;

View File

@ -0,0 +1,96 @@
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import { useTranslation, Trans } from "react-i18next";
import Text from "@docspace/components/text";
import { inject, observer } from "mobx-react";
import Button from "@docspace/components/button";
import { smallTablet } from "@docspace/components/utils/device";
const StyledBody = styled.div`
max-width: 272px;
margin: 0 auto;
@media ${smallTablet} {
max-width: 520px;
}
.total-tariff_user {
display: flex;
align-items: center;
justify-content: center;
background: #f3f4f4;
p:first-child {
margin-right: 8px;
}
p {
margin-bottom: 5px;
margin-top: 5px;
}
}
.total-tariff_price {
display: flex;
justify-content: center;
.total-tariff_price-text {
margin-bottom: 10px;
border-bottom: 1px solid #eceef1;
}
.total-tariff_month-text {
margin: auto 0;
}
}
button {
width: 100%;
}
`;
const TotalTariffContainer = ({ t, usersCount, price }) => {
useEffect(() => {}, []);
const totalPrice = price * usersCount;
return (
<StyledBody>
<div className="total-tariff_user">
<Text fontSize="16px" textAlign="center" isBold noSelect>
{price}
</Text>
<Text
fontSize="11px"
textAlign="center"
fontWeight={600}
color={"#A3A9AE"}
noSelect
>
{t("PerUserMonth")}
</Text>
</div>
<div className="total-tariff_price">
<Text
fontSize="48px"
isBold
textAlign={"center"}
className="total-tariff_price-text"
noSelect
>
{totalPrice}
</Text>
<Text
fontSize="16px"
isBold
textAlign={"center"}
className="total-tariff_month-text"
noSelect
>
{"/month"}
</Text>
</div>
<Button label={t("UpgradeNow")} size={"medium"} primary />
</StyledBody>
);
};
export default inject(({}) => {
return {};
})(observer(TotalTariffContainer));

View File

@ -532,3 +532,10 @@ export function getMetadata() {
export function getOforms(url) {
return axios.get(url);
}
export function getPortalQuota() {
return request({
method: "get",
url: `/settings/quota`,
});
}

View File

@ -25,6 +25,8 @@ class AuthStore {
providers = [];
isInit = false;
quota = {};
constructor() {
this.userStore = new UserStore();
@ -284,6 +286,11 @@ class AuthStore {
return promise;
};
setQuota = async () => {
const res = await api.settings.getPortalQuota();
if (res) this.quota = res;
};
}
export default new AuthStore();

View File

@ -2622,6 +2622,10 @@ const Base = {
textColorError: red,
},
payments: {
linkColor: link,
},
paymentsEnterprise: {
background: grayLight,

View File

@ -2632,6 +2632,10 @@ const Dark = {
textColorError: red,
},
payments: {
linkColor: "#E06A1B",
},
paymentsEnterprise: {
background: black,