mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
feat: optimize the onboarding subscription experience.
This commit is contained in:
@@ -48,7 +48,7 @@ const GroupStyled = styled(Box)`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-items: ${(props: GroupProps) => (props.align || 'center')};
|
||||
flex-wrap: ${(props: GroupProps) => (props.noWrap ? 'nowrap' : 'wrap')};
|
||||
justify-content: ${(props: GroupProps) =>
|
||||
GROUP_POSITIONS[props.position || 'left']};
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
.root{
|
||||
border-radius: 5px;
|
||||
padding: 40px 15px;
|
||||
position: relative;
|
||||
border: 1px solid #D8DEE4;
|
||||
padding-top: 45px;
|
||||
|
||||
&.isFeatured {
|
||||
background-color: #F5F6F8;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
.featuredBox {
|
||||
background-color: #A3ACBA;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
.label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #2F343C;
|
||||
|
||||
}
|
||||
.description{
|
||||
font-size: 14px;
|
||||
color: #687385;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.buttonCTA {
|
||||
min-height: 34px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.features {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.priceRoot{
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.price {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
color: #404854;
|
||||
}
|
||||
|
||||
.pricePer{
|
||||
color: #738091;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
125
packages/webapp/src/components/PricingPlan/PricingPlan.tsx
Normal file
125
packages/webapp/src/components/PricingPlan/PricingPlan.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Button, ButtonProps, Intent } from '@blueprintjs/core';
|
||||
import clsx from 'classnames';
|
||||
import { Box, Group, Stack } from '../Layout';
|
||||
import styles from './PricingPlan.module.scss';
|
||||
import { CheckCircled } from '@/icons/CheckCircled';
|
||||
|
||||
export interface PricingPlanProps {
|
||||
featured?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a pricing plan.
|
||||
* @param featured - Whether the plan is featured.
|
||||
* @param children - The content of the plan.
|
||||
*/
|
||||
export const PricingPlan = ({ featured, children }: PricingPlanProps) => {
|
||||
return (
|
||||
<Stack
|
||||
spacing={8}
|
||||
className={clsx(styles.root, { [styles.isFeatured]: featured })}
|
||||
>
|
||||
<>{children}</>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a featured section within a pricing plan.
|
||||
* @param children - The content of the featured section.
|
||||
*/
|
||||
PricingPlan.Featured = ({ children }: { children: React.ReactNode }) => {
|
||||
return <Box className={styles.featuredBox}>{children}</Box>;
|
||||
};
|
||||
|
||||
export interface PricingHeaderProps {
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the header of a pricing plan.
|
||||
* @param label - The label of the plan.
|
||||
* @param description - The description of the plan.
|
||||
*/
|
||||
PricingPlan.Header = ({ label, description }: PricingHeaderProps) => {
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<h4 className={styles.label}>{label}</h4>
|
||||
{description && <p className={styles.description}>{description}</p>}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PricingPriceProps {
|
||||
price: string;
|
||||
subPrice: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the price of a pricing plan.
|
||||
* @param price - The main price of the plan.
|
||||
* @param subPrice - The sub-price of the plan.
|
||||
*/
|
||||
PricingPlan.Price = ({ price, subPrice }: PricingPriceProps) => {
|
||||
return (
|
||||
<Stack spacing={6} className={styles.priceRoot}>
|
||||
<h4 className={styles.price}>{price}</h4>
|
||||
<span className={styles.pricePer}>{subPrice}</span>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PricingBuyButtonProps extends ButtonProps {}
|
||||
|
||||
/**
|
||||
* Displays a buy button within a pricing plan.
|
||||
* @param children - The content of the button.
|
||||
* @param props - Additional button props.
|
||||
*/
|
||||
PricingPlan.BuyButton = ({ children, ...props }: PricingBuyButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
{...props}
|
||||
fill={true}
|
||||
className={styles.buttonCTA}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PricingFeaturesProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a list of features within a pricing plan.
|
||||
* @param children - The list of features.
|
||||
*/
|
||||
PricingPlan.Features = ({ children }: PricingFeaturesProps) => {
|
||||
return (
|
||||
<Stack spacing={10} className={styles.features}>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PricingFeatureLineProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a single feature line within a list of features.
|
||||
* @param children - The content of the feature line.
|
||||
*/
|
||||
PricingPlan.FeatureLine = ({ children }: PricingFeatureLineProps) => {
|
||||
return (
|
||||
<Group noWrap spacing={12}>
|
||||
<CheckCircled height={12} width={12} />
|
||||
<Box className={styles.featureItem}>{children}</Box>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -1,136 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { T } from '@/components';
|
||||
import { saveInvoke } from '@/utils';
|
||||
|
||||
import '@/style/pages/Subscription/PlanRadio.scss';
|
||||
import '@/style/pages/Subscription/PlanPeriodRadio.scss';
|
||||
|
||||
export function SubscriptionPlans({ value, plans, onSelect }) {
|
||||
const handleSelect = (value) => {
|
||||
onSelect && onSelect(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'plan-radios'}>
|
||||
{plans.map((plan) => (
|
||||
<SubscriptionPlan
|
||||
name={plan.name}
|
||||
description={plan.description}
|
||||
slug={plan.slug}
|
||||
price={plan.price}
|
||||
currencyCode={plan.currencyCode}
|
||||
value={plan.slug}
|
||||
onSelected={handleSelect}
|
||||
selectedOption={value}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SubscriptionPlan({
|
||||
name,
|
||||
description,
|
||||
price,
|
||||
currencyCode,
|
||||
|
||||
value,
|
||||
selectedOption,
|
||||
onSelected,
|
||||
}) {
|
||||
const handlePlanClick = () => {
|
||||
saveInvoke(onSelected, value);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
id={'basic-plan'}
|
||||
className={classNames('plan-radio', {
|
||||
'is-selected': selectedOption === value,
|
||||
})}
|
||||
onClick={handlePlanClick}
|
||||
>
|
||||
<div className={'plan-radio__header'}>
|
||||
<div className={'plan-radio__name'}>{name}</div>
|
||||
</div>
|
||||
|
||||
<div className={'plan-radio__description'}>
|
||||
<ul>
|
||||
{description.map((line) => (
|
||||
<li>{line}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={'plan-radio__price'}>
|
||||
<span className={'plan-radio__amount'}>
|
||||
{price} {currencyCode}
|
||||
</span>
|
||||
<span className={'plan-radio__period'}>
|
||||
<T id={'monthly'} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription periods.
|
||||
*/
|
||||
export function SubscriptionPeriods({ periods, selectedPeriod, onPeriodSelect }) {
|
||||
const handleSelected = (value) => {
|
||||
saveInvoke(onPeriodSelect, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'plan-periods'}>
|
||||
{periods.map((period) => (
|
||||
<SubscriptionPeriod
|
||||
period={period.slug}
|
||||
label={period.label}
|
||||
onSelected={handleSelected}
|
||||
price={period.price}
|
||||
selectedPeriod={selectedPeriod}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Billing period.
|
||||
*/
|
||||
export function SubscriptionPeriod({
|
||||
// #ownProps
|
||||
label,
|
||||
selectedPeriod,
|
||||
onSelected,
|
||||
period,
|
||||
price,
|
||||
currencyCode,
|
||||
}) {
|
||||
const handlePeriodClick = () => {
|
||||
saveInvoke(onSelected, period);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
id={`plan-period-${period}`}
|
||||
className={classNames(
|
||||
{ 'is-selected': period === selectedPeriod },
|
||||
'period-radio',
|
||||
)}
|
||||
onClick={handlePeriodClick}
|
||||
>
|
||||
<span className={'period-radio__label'}>{label}</span>
|
||||
|
||||
<div className={'period-radio__price'}>
|
||||
<span className={'period-radio__amount'}>
|
||||
{price} {currencyCode}
|
||||
</span>
|
||||
<span className={'period-radio__period'}>{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,6 @@ export * from './PdfPreview';
|
||||
export * from './Details';
|
||||
export * from './TotalLines/index';
|
||||
export * from './Alert';
|
||||
export * from './Subscriptions';
|
||||
export * from './Dashboard';
|
||||
export * from './Drawer';
|
||||
export * from './Forms';
|
||||
|
||||
Reference in New Issue
Block a user