Files
mtdb_movie/common/resources/client/billing/pricing-table/pricing-table.tsx
maher 703f50a09d
Some checks failed
Build / run (push) Has been cancelled
first commit
2025-10-29 11:42:25 +01:00

179 lines
5.4 KiB
TypeScript
Executable File

import {AnimatePresence, m} from 'framer-motion';
import {Fragment} from 'react';
import {opacityAnimation} from '@common/ui/animation/opacity-animation';
import {Skeleton} from '@common/ui/skeleton/skeleton';
import {useProducts} from '@common/billing/pricing-table/use-products';
import {Product} from '@common/billing/product';
import {
findBestPrice,
UpsellBillingCycle,
} from '@common/billing/pricing-table/find-best-price';
import {useAuth} from '@common/auth/use-auth';
import clsx from 'clsx';
import {Chip} from '@common/ui/forms/input-field/chip-field/chip';
import {Trans} from '@common/i18n/trans';
import {FormattedPrice} from '@common/i18n/formatted-price';
import {Button} from '@common/ui/buttons/button';
import {Link} from 'react-router-dom';
import {setInLocalStorage} from '@common/utils/hooks/local-storage';
import {ProductFeatureList} from '@common/billing/pricing-table/product-feature-list';
interface PricingTableProps {
selectedCycle: UpsellBillingCycle;
className?: string;
productLoader?: string;
}
export function PricingTable({
selectedCycle,
className,
productLoader,
}: PricingTableProps) {
const query = useProducts(productLoader);
return (
<div
className={clsx(
'flex flex-col items-stretch gap-24 overflow-x-auto overflow-y-visible pb-20 md:flex-row md:justify-center',
className,
)}
>
<AnimatePresence initial={false} mode="wait">
{query.data ? (
<PlanList
key="plan-list"
plans={query.data.products}
selectedPeriod={selectedCycle}
/>
) : (
<SkeletonLoader key="skeleton-loader" />
)}
</AnimatePresence>
</div>
);
}
interface PlanListProps {
plans: Product[];
selectedPeriod: UpsellBillingCycle;
}
function PlanList({plans, selectedPeriod}: PlanListProps) {
const {isLoggedIn, isSubscribed} = useAuth();
const filteredPlans = plans.filter(plan => !plan.hidden);
return (
<Fragment>
{filteredPlans.map((plan, index) => {
const isFirst = index === 0;
const isLast = index === filteredPlans.length - 1;
const price = findBestPrice(selectedPeriod, plan.prices);
let upgradeRoute;
if (!isLoggedIn) {
upgradeRoute = `/register?redirectFrom=pricing`;
}
if (isSubscribed) {
upgradeRoute = `/change-plan/${plan.id}/${price?.id}/confirm`;
}
if (isLoggedIn && !plan.free) {
upgradeRoute = `/checkout/${plan.id}/${price?.id}`;
}
return (
<m.div
key={plan.id}
{...opacityAnimation}
className={clsx(
'w-full rounded-panel border bg px-28 py-28 shadow-lg md:min-w-240 md:max-w-350',
isFirst && 'ml-auto',
isLast && 'mr-auto',
)}
>
<div className="mb-32">
<Chip
radius="rounded"
size="sm"
className={clsx(
'mb-20 w-min',
!plan.recommended && 'invisible',
)}
>
<Trans message="Most popular" />
</Chip>
<div className="mb-12 text-xl font-semibold">
<Trans message={plan.name} />
</div>
<div className="text-sm text-muted">
<Trans message={plan.description} />
</div>
</div>
<div>
{price ? (
<FormattedPrice
priceClassName="font-bold text-4xl"
periodClassName="text-muted text-xs"
variant="separateLine"
price={price}
/>
) : (
<div className="text-4xl font-bold">
<Trans message="Free" />
</div>
)}
<div className="mt-60">
<Button
variant={plan.recommended ? 'flat' : 'outline'}
color="primary"
className="w-full"
size="md"
elementType={upgradeRoute ? Link : undefined}
disabled={!upgradeRoute}
onClick={() => {
if (isLoggedIn || !price || !plan) return;
setInLocalStorage('be.onboarding.selected', {
productId: plan.id,
priceId: price.id,
});
}}
to={upgradeRoute}
>
{plan.free ? (
<Trans message="Get started" />
) : (
<Trans message="Upgrade" />
)}
</Button>
</div>
<ProductFeatureList product={plan} />
</div>
</m.div>
);
})}
</Fragment>
);
}
function SkeletonLoader() {
return (
<Fragment>
<PlanSkeleton key="skeleton-1" />
<PlanSkeleton key="skeleton-2" />
<PlanSkeleton key="skeleton-3" />
</Fragment>
);
}
function PlanSkeleton() {
return (
<m.div
{...opacityAnimation}
className="w-full rounded-lg border px-28 py-90 shadow-lg md:max-w-350"
>
<Skeleton className="my-10" />
<Skeleton className="mb-40" />
<Skeleton className="mb-40 h-30" />
<Skeleton className="mb-40 h-40" />
<Skeleton className="mb-20" />
<Skeleton />
<Skeleton />
</m.div>
);
}