[WEB-1960]: chore: upgrade to plane paid plans modal. (#5149)
This commit is contained in:
parent
281948c1ce
commit
cfc70622d6
9 changed files with 328 additions and 29 deletions
|
|
@ -8,4 +8,6 @@ export enum EModalWidth {
|
||||||
XXL = "sm:max-w-2xl",
|
XXL = "sm:max-w-2xl",
|
||||||
XXXL = "sm:max-w-3xl",
|
XXXL = "sm:max-w-3xl",
|
||||||
XXXXL = "sm:max-w-4xl",
|
XXXXL = "sm:max-w-4xl",
|
||||||
|
VXL = "sm:max-w-5xl",
|
||||||
|
VIXL = "sm:max-w-6xl",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,17 @@ type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
position?: EModalPosition;
|
position?: EModalPosition;
|
||||||
width?: EModalWidth;
|
width?: EModalWidth;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
export const ModalCore: React.FC<Props> = (props) => {
|
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 (
|
return (
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
|
|
@ -44,7 +52,8 @@ export const ModalCore: React.FC<Props> = (props) => {
|
||||||
<Dialog.Panel
|
<Dialog.Panel
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative transform rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all w-full",
|
"relative transform rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all w-full",
|
||||||
width
|
width,
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
26
turbo.json
26
turbo.json
|
|
@ -22,36 +22,28 @@
|
||||||
"SENTRY_PROJECT_ID",
|
"SENTRY_PROJECT_ID",
|
||||||
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
||||||
"NEXT_PUBLIC_SENTRY_DSN",
|
"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": {
|
"tasks": {
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": [
|
"dependsOn": ["^build"],
|
||||||
"^build"
|
"outputs": [".next/**", "dist/**"]
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
".next/**",
|
|
||||||
"dist/**"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"develop": {
|
"develop": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"persistent": true,
|
"persistent": true,
|
||||||
"dependsOn": [
|
"dependsOn": ["^build"]
|
||||||
"^build"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"persistent": true,
|
"persistent": true,
|
||||||
"dependsOn": [
|
"dependsOn": ["^build"]
|
||||||
"^build"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"dependsOn": [
|
"dependsOn": ["^build"],
|
||||||
"^build"
|
|
||||||
],
|
|
||||||
"outputs": []
|
"outputs": []
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,35 @@
|
||||||
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Button, Tooltip } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// assets
|
// assets
|
||||||
import packageJson from "package.json";
|
import packageJson from "package.json";
|
||||||
|
// local components
|
||||||
|
import { PaidPlanUpgradeModal } from "./upgrade";
|
||||||
|
|
||||||
export const WorkspaceEditionBadge = observer(() => {
|
export const WorkspaceEditionBadge = observer(() => {
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
|
// states
|
||||||
|
const [isPaidPlanPurchaseModalOpen, setIsPaidPlanPurchaseModalOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
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">
|
<PaidPlanUpgradeModal
|
||||||
Community
|
isOpen={isPaidPlanPurchaseModalOpen}
|
||||||
</div>
|
handleClose={() => setIsPaidPlanPurchaseModalOpen(false)}
|
||||||
</Tooltip>
|
/>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
3
web/ce/components/workspace/upgrade/index.tsx
Normal file
3
web/ce/components/workspace/upgrade/index.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./pro-plan-upgrade";
|
||||||
|
export * from "./one-plan-upgrade";
|
||||||
|
export * from "./paid-plans-upgrade-modal";
|
||||||
55
web/ce/components/workspace/upgrade/one-plan-upgrade.tsx
Normal file
55
web/ce/components/workspace/upgrade/one-plan-upgrade.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
113
web/ce/components/workspace/upgrade/paid-plans-upgrade-modal.tsx
Normal file
113
web/ce/components/workspace/upgrade/paid-plans-upgrade-modal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
111
web/ce/components/workspace/upgrade/pro-plan-upgrade.tsx
Normal file
111
web/ce/components/workspace/upgrade/pro-plan-upgrade.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
// type
|
// router from n-progress-bar
|
||||||
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
|
||||||
// router from next-nprogress-bar
|
|
||||||
import { useRouter } from "@/lib/n-progress";
|
import { useRouter } from "@/lib/n-progress";
|
||||||
|
|
||||||
export const useAppRouter = (): AppRouterInstance => useRouter();
|
export const useAppRouter = () => useRouter();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue