mirror of
https://github.com/makeplane/plane.git
synced 2026-01-20 13:20:55 -06:00
[WEB-1960]: chore: upgrade to plane paid plans modal. (#5149)
This commit is contained in:
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
26
turbo.json
26
turbo.json
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
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
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user