156
common/resources/client/billing/billing-page/change-plan-page.tsx
Executable file
156
common/resources/client/billing/billing-page/change-plan-page.tsx
Executable file
@@ -0,0 +1,156 @@
|
||||
import {Breadcrumb} from '../../ui/breadcrumbs/breadcrumb';
|
||||
import {BreadcrumbItem} from '../../ui/breadcrumbs/breadcrumb-item';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
import {useNavigate} from '../../utils/hooks/use-navigate';
|
||||
import {BillingPlanPanel} from './billing-plan-panel';
|
||||
import {Product} from '../product';
|
||||
import {
|
||||
findBestPrice,
|
||||
UpsellBillingCycle,
|
||||
} from '../pricing-table/find-best-price';
|
||||
import {Fragment, useState} from 'react';
|
||||
import {FormattedPrice} from '../../i18n/formatted-price';
|
||||
import {Button} from '../../ui/buttons/button';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {useProducts} from '../pricing-table/use-products';
|
||||
import {Price} from '../price';
|
||||
import {useBillingUser} from './use-billing-user';
|
||||
import {CheckIcon} from '../../icons/material/Check';
|
||||
import {Skeleton} from '../../ui/skeleton/skeleton';
|
||||
import {AnimatePresence, m} from 'framer-motion';
|
||||
import {BillingCycleRadio} from '../pricing-table/billing-cycle-radio';
|
||||
import {opacityAnimation} from '../../ui/animation/opacity-animation';
|
||||
|
||||
export function ChangePlanPage() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Fragment>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbItem isLink onSelected={() => navigate('/billing')}>
|
||||
<Trans message="Billing" />
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem>
|
||||
<Trans message="Plans" />
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
<h1 className="my-32 text-3xl font-bold md:my-64">
|
||||
<Trans message="Change your plan" />
|
||||
</h1>
|
||||
<BillingPlanPanel title={<Trans message="Available plans" />}>
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
<PlanList />
|
||||
</AnimatePresence>
|
||||
</BillingPlanPanel>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function PlanList() {
|
||||
const query = useProducts();
|
||||
const [selectedCycle, setSelectedCycle] =
|
||||
useState<UpsellBillingCycle>('monthly');
|
||||
|
||||
if (query.isLoading) {
|
||||
return <PlanSkeleton key="plan-skeleton" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key="plan-list">
|
||||
<BillingCycleRadio
|
||||
products={query.data?.products}
|
||||
selectedCycle={selectedCycle}
|
||||
onChange={setSelectedCycle}
|
||||
className="mb-20"
|
||||
size="md"
|
||||
/>
|
||||
{query.data?.products.map(plan => {
|
||||
const price = findBestPrice(selectedCycle, plan.prices);
|
||||
if (!price || plan.hidden) return null;
|
||||
return (
|
||||
<m.div
|
||||
{...opacityAnimation}
|
||||
key={plan.id}
|
||||
className="justify-between gap-40 border-b py-32 md:flex"
|
||||
>
|
||||
<div className="mb-40 md:mb-0">
|
||||
<div className="text-xl font-bold">{plan.name}</div>
|
||||
<FormattedPrice price={price} className="text-lg" />
|
||||
<div className="mt-12 text-base">{plan.description}</div>
|
||||
<FeatureList plan={plan} />
|
||||
</div>
|
||||
<ContinueButton product={plan} price={price} />
|
||||
</m.div>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
interface FeatureListProps {
|
||||
plan: Product;
|
||||
}
|
||||
function FeatureList({plan}: FeatureListProps) {
|
||||
if (!plan.feature_list.length) return null;
|
||||
return (
|
||||
<div className="mt-32">
|
||||
<div className="mb-10 text-sm font-semibold">
|
||||
<Trans message="What's included" />
|
||||
</div>
|
||||
{plan.feature_list.map(feature => (
|
||||
<div key={feature} className="flex items-center gap-10 text-sm">
|
||||
<CheckIcon className="text-positive" size="sm" />
|
||||
<Trans message={feature} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ContinueButtonProps {
|
||||
product: Product;
|
||||
price: Price;
|
||||
}
|
||||
function ContinueButton({product, price}: ContinueButtonProps) {
|
||||
const {subscription} = useBillingUser();
|
||||
if (!subscription?.price || !subscription?.product) return null;
|
||||
|
||||
if (
|
||||
subscription.product_id === product.id &&
|
||||
subscription.price_id === price.id
|
||||
) {
|
||||
return (
|
||||
<div className="flex w-[168px] items-center justify-center gap-10 text-muted">
|
||||
<CheckIcon size="md" />
|
||||
<Trans message="Current plan" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="flat"
|
||||
color="primary"
|
||||
className="w-[168px]"
|
||||
size="md"
|
||||
elementType={Link}
|
||||
to={`/billing/change-plan/${product.id}/${price.id}/confirm`}
|
||||
>
|
||||
<Trans message="Continue" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function PlanSkeleton() {
|
||||
return (
|
||||
<m.div
|
||||
key="plan-skeleton"
|
||||
{...opacityAnimation}
|
||||
className="border-b py-32 text-2xl"
|
||||
>
|
||||
<Skeleton className="mb-8" />
|
||||
<Skeleton className="mb-14" />
|
||||
<Skeleton className="mb-24" />
|
||||
<Skeleton className="mb-12" />
|
||||
</m.div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user