[WEB-1960]: chore: upgrade to plane paid plans modal. (#5149)

This commit is contained in:
Prateek Shourya
2024-07-18 15:45:37 +05:30
committed by GitHub
parent 281948c1ce
commit cfc70622d6
9 changed files with 328 additions and 29 deletions

View File

@@ -8,4 +8,6 @@ export enum EModalWidth {
XXL = "sm:max-w-2xl",
XXXL = "sm:max-w-3xl",
XXXXL = "sm:max-w-4xl",
VXL = "sm:max-w-5xl",
VIXL = "sm:max-w-6xl",
}

View File

@@ -11,9 +11,17 @@ type Props = {
isOpen: boolean;
position?: EModalPosition;
width?: EModalWidth;
className?: string;
};
export const ModalCore: React.FC<Props> = (props) => {
const { children, handleClose, isOpen, position = EModalPosition.CENTER, width = EModalWidth.XXL } = props;
const {
children,
handleClose,
isOpen,
position = EModalPosition.CENTER,
width = EModalWidth.XXL,
className = "",
} = props;
return (
<Transition.Root show={isOpen} as={Fragment}>
@@ -44,7 +52,8 @@ export const ModalCore: React.FC<Props> = (props) => {
<Dialog.Panel
className={cn(
"relative transform rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all w-full",
width
width,
className
)}
>
{children}

View File

@@ -22,36 +22,28 @@
"SENTRY_PROJECT_ID",
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
"NEXT_PUBLIC_SENTRY_DSN",
"SENTRY_MONITORING_ENABLED"
"SENTRY_MONITORING_ENABLED",
"NEXT_PUBLIC_PRO_PLAN_MONTHLY_PAYMENT_URL",
"NEXT_PUBLIC_PRO_PLAN_YEARLY_PAYMENT_URL",
"NEXT_PUBLIC_PLANE_ONE_PAYMENT_URL"
],
"tasks": {
"build": {
"dependsOn": [
"^build"
],
"outputs": [
".next/**",
"dist/**"
]
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"develop": {
"cache": false,
"persistent": true,
"dependsOn": [
"^build"
]
"dependsOn": ["^build"]
},
"dev": {
"cache": false,
"persistent": true,
"dependsOn": [
"^build"
]
"dependsOn": ["^build"]
},
"test": {
"dependsOn": [
"^build"
],
"dependsOn": ["^build"],
"outputs": []
},
"lint": {

View File

@@ -1,19 +1,35 @@
import { useState } from "react";
import { observer } from "mobx-react";
// ui
import { Tooltip } from "@plane/ui";
import { Button, Tooltip } from "@plane/ui";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
// assets
import packageJson from "package.json";
// local components
import { PaidPlanUpgradeModal } from "./upgrade";
export const WorkspaceEditionBadge = observer(() => {
const { isMobile } = usePlatformOS();
// states
const [isPaidPlanPurchaseModalOpen, setIsPaidPlanPurchaseModalOpen] = useState(false);
return (
<Tooltip tooltipContent={`Version: v${packageJson.version}`} isMobile={isMobile}>
<div className="w-full cursor-default rounded-md bg-green-500/10 px-2 py-1 text-center text-xs font-medium text-green-500 outline-none leading-6">
Community
</div>
</Tooltip>
<>
<PaidPlanUpgradeModal
isOpen={isPaidPlanPurchaseModalOpen}
handleClose={() => setIsPaidPlanPurchaseModalOpen(false)}
/>
<Tooltip tooltipContent={`Version: v${packageJson.version}`} isMobile={isMobile}>
<Button
tabIndex={-1}
variant="accent-primary"
className="w-full cursor-pointer rounded-2xl px-4 py-1.5 text-center text-sm font-medium outline-none"
onClick={() => setIsPaidPlanPurchaseModalOpen(true)}
>
Upgrade
</Button>
</Tooltip>
</>
);
});

View File

@@ -0,0 +1,3 @@
export * from "./pro-plan-upgrade";
export * from "./one-plan-upgrade";
export * from "./paid-plans-upgrade-modal";

View File

@@ -0,0 +1,55 @@
import { FC } from "react";
import { CheckCircle } from "lucide-react";
// helpers
import { cn } from "@/helpers/common.helper";
export type OnePlanUpgradeProps = {
features: string[];
verticalFeatureList?: boolean;
extraFeatures?: string | React.ReactNode;
};
export const OnePlanUpgrade: FC<OnePlanUpgradeProps> = (props) => {
const { features, verticalFeatureList = false, extraFeatures } = props;
// env
const PLANE_ONE_PAYMENT_URL = process.env.NEXT_PUBLIC_PLANE_ONE_PAYMENT_URL ?? "https://plane.so/one";
return (
<div className="py-4 px-2 border border-custom-border-90 rounded-xl bg-custom-background-90">
<div className="flex w-full justify-center h-10" />
<div className="pt-6 pb-4 text-center font-semibold">
<div className="text-2xl">Plane One</div>
<div className="text-3xl">$799</div>
<div className="text-sm text-custom-text-300">for two years support and updates</div>
</div>
<div className="flex justify-center w-full">
<a
href={PLANE_ONE_PAYMENT_URL}
target="_blank"
className="relative inline-flex items-center justify-center w-56 px-4 py-2.5 text-white text-sm font-medium border border-[#525252] bg-gradient-to-r from-[#353535] via-[#1111118C] to-[#21212153] rounded-lg focus:outline-none"
>
Upgrade to One
</a>
</div>
<div className="px-2 pt-6 pb-2">
<div className="p-2 text-sm font-semibold">Everything in Free +</div>
<ul className="w-full grid grid-cols-12 gap-x-4">
{features.map((feature) => (
<li
key={feature}
className={cn("col-span-12 relative rounded-md p-2 flex", {
"sm:col-span-6": !verticalFeatureList,
})}
>
<p className="w-full text-sm font-medium leading-5 flex items-center">
<CheckCircle className="h-4 w-4 mr-4 text-custom-text-300 flex-shrink-0" />
<span className="text-custom-text-200 truncate">{feature}</span>
</p>
</li>
))}
</ul>
{extraFeatures && <div>{extraFeatures}</div>}
</div>
</div>
);
};

View File

@@ -0,0 +1,113 @@
import { FC } from "react";
// types
import { CircleX } from "lucide-react";
// services
import { EModalWidth, ModalCore } from "@plane/ui";
// plane web components
import { cn } from "@/helpers/common.helper";
// local components
import { OnePlanUpgrade } from "./one-plan-upgrade";
import { ProPlanUpgrade } from "./pro-plan-upgrade";
const PRO_PLAN_FEATURES = [
"More Cycles features",
"Full Time Tracking + Bulk Ops",
"Workflow manager",
"Automations",
"Popular integrations",
"Plane AI",
];
const ONE_PLAN_FEATURES = [
"OIDC + SAML for SSO",
"Active Cycles",
"Real-time collab + public views and page",
"Link pages in issues and vice-versa",
"Time-tracking + limited bulk ops",
"Docker, Kubernetes and more",
];
const FREE_PLAN_UPGRADE_FEATURES = [
"OIDC + SAML for SSO",
"Time tracking and bulk ops",
"Integrations",
"Public views and pages",
];
export type PaidPlanUpgradeModalProps = {
isOpen: boolean;
handleClose: () => void;
};
export const PaidPlanUpgradeModal: FC<PaidPlanUpgradeModalProps> = (props) => {
const { isOpen, handleClose } = props;
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} width={EModalWidth.VIXL} className="rounded-2xl">
<div className="p-10 max-h-[90vh] overflow-auto">
<div className="grid grid-cols-12 gap-6">
<div className="col-span-12 md:col-span-4">
<div className="text-3xl font-bold leading-8 flex">Upgrade to a paid plan and unlock missing features.</div>
<div className="mt-4 mb-12">
<p className="text-sm mb-4 pr-8 text-custom-text-100">
Active Cycles, time tracking, bulk ops, and other features are waiting for you on one of our paid plans.
Upgrade today to unlock features your teams need yesterday.
</p>
</div>
{/* Free plan details */}
<div className="py-4 px-2 border border-custom-border-90 rounded-xl">
<div className="py-2 px-3">
<span className="px-2 py-1 bg-custom-background-90 text-sm text-custom-text-300 font-medium rounded">
Your plan
</span>
</div>
<div className="px-4 py-2 font-semibold">
<div className="text-3xl">Free</div>
<div className="text-sm text-custom-text-300">$0 a user per month</div>
</div>
<div className="px-2 pt-2 pb-3">
<ul className="w-full grid grid-cols-12 gap-x-4">
{FREE_PLAN_UPGRADE_FEATURES.map((feature) => (
<li key={feature} className={cn("col-span-12 relative rounded-md p-2 flex")}>
<p className="w-full text-sm font-medium leading-5 flex items-center">
<CircleX className="h-4 w-4 mr-4 text-red-500 flex-shrink-0" />
<span className="text-custom-text-200 truncate">{feature}</span>
</p>
</li>
))}
</ul>
</div>
</div>
</div>
<div className="col-span-12 md:col-span-4">
<ProPlanUpgrade
basePlan="One"
features={PRO_PLAN_FEATURES}
verticalFeatureList
extraFeatures={
<p className="pt-1.5 text-center text-xs text-custom-primary-200 font-semibold underline">
<a href="https://plane.so/pro" target="_blank">
See full features list
</a>
</p>
}
/>
</div>
<div className="col-span-12 md:col-span-4">
<OnePlanUpgrade
features={ONE_PLAN_FEATURES}
verticalFeatureList
extraFeatures={
<p className="pt-1.5 text-center text-xs text-custom-primary-200 font-semibold underline">
<a href="https://plane.so/one" target="_blank">
See full features list
</a>
</p>
}
/>
</div>
</div>
</div>
</ModalCore>
);
};

View File

@@ -0,0 +1,111 @@
import { FC, useState } from "react";
import { CheckCircle } from "lucide-react";
import { Tab } from "@headlessui/react";
// helpers
import { cn } from "@/helpers/common.helper";
export type ProPlanUpgradeProps = {
basePlan: "Free" | "One";
features: string[];
verticalFeatureList?: boolean;
extraFeatures?: string | React.ReactNode;
};
type TProPiceFrequency = "month" | "year";
type TProPlanPrice = {
key: string;
price: string;
recurring: TProPiceFrequency;
};
const PRO_PLAN_PRICES: TProPlanPrice[] = [
{ key: "monthly", price: "$7", recurring: "month" },
{ key: "yearly", price: "$5", recurring: "year" },
];
export const ProPlanUpgrade: FC<ProPlanUpgradeProps> = (props) => {
const { basePlan, features, verticalFeatureList = false, extraFeatures } = props;
// states
const [selectedPlan, setSelectedPlan] = useState<TProPiceFrequency>("month");
// env
const PRO_PLAN_MONTHLY_PAYMENT_URL = process.env.NEXT_PUBLIC_PRO_PLAN_MONTHLY_PAYMENT_URL ?? "https://plane.so/pro";
const PRO_PLAN_YEARLY_PAYMENT_URL = process.env.NEXT_PUBLIC_PRO_PLAN_YEARLY_PAYMENT_URL ?? "https://plane.so/pro";
return (
<div className="py-4 px-2 border border-custom-primary-200/30 rounded-xl bg-custom-primary-200/5">
<Tab.Group>
<div className="flex w-full justify-center h-10">
<Tab.List className="flex space-x-1 rounded-lg bg-custom-primary-200/10 p-1 w-60">
{PRO_PLAN_PRICES.map((price: TProPlanPrice) => (
<Tab
key={price.key}
className={({ selected }) =>
cn(
"w-full rounded-lg py-1.5 text-sm font-medium leading-5",
selected
? "bg-custom-background-100 text-custom-primary-300 shadow"
: "hover:bg-custom-primary-100/5 text-custom-text-300 hover:text-custom-text-200"
)
}
onClick={() => setSelectedPlan(price.recurring)}
>
<>
{price.recurring === "month" && ("Monthly" as string)}
{price.recurring === "year" && ("Yearly" as string)}
{price.recurring === "year" && (
<span className="bg-gradient-to-r from-[#C78401] to-[#896828] text-white rounded-full px-2 py-1 ml-1 text-xs">
-28%
</span>
)}
</>
</Tab>
))}
</Tab.List>
</div>
<Tab.Panels>
{PRO_PLAN_PRICES.map((price: TProPlanPrice) => (
<Tab.Panel key={price.key}>
<div className="pt-6 pb-4 text-center font-semibold">
<div className="text-2xl">Plane Pro</div>
<div className="text-3xl">
{price.recurring === "month" && "$7"}
{price.recurring === "year" && "$5"}
</div>
<div className="text-sm text-custom-text-300">a user per month</div>
</div>
<div className="flex justify-center w-full">
<a
href={selectedPlan === "month" ? PRO_PLAN_MONTHLY_PAYMENT_URL : PRO_PLAN_YEARLY_PAYMENT_URL}
target="_blank"
className="relative inline-flex items-center justify-center w-56 px-4 py-2.5 text-white text-sm font-medium border border-[#E9DBBF99]/60 bg-gradient-to-r from-[#C78401] to-[#896828] rounded-lg focus:outline-none"
>
Upgrade to Pro
</a>
</div>
<div className="px-2 pt-6 pb-2">
<div className="p-2 text-sm font-semibold">{`Everything in ${basePlan} +`}</div>
<ul className="grid grid-cols-12 gap-x-4">
{features.map((feature) => (
<li
key={feature}
className={cn("col-span-12 relative rounded-md p-2 flex", {
"sm:col-span-6": !verticalFeatureList,
})}
>
<p className="w-full text-sm font-medium leading-5 flex items-center line-clamp-1">
<CheckCircle className="h-4 w-4 mr-4 text-custom-text-300 flex-shrink-0" />
<span className="text-custom-text-200 truncate">{feature}</span>
</p>
</li>
))}
</ul>
{extraFeatures && <div>{extraFeatures}</div>}
</div>
</Tab.Panel>
))}
</Tab.Panels>
</Tab.Group>
</div>
);
};

View File

@@ -1,6 +1,4 @@
// type
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
// router from next-nprogress-bar
// router from n-progress-bar
import { useRouter } from "@/lib/n-progress";
export const useAppRouter = (): AppRouterInstance => useRouter();
export const useAppRouter = () => useRouter();