feat(cloud): enable free trial (#236)
* feat(cloud): add free trial * fix: center workspace name icon when collapsed * feat: add 14 day free trial notice to pricing page * feat: hide upgrade button when on active subscription * chore: translations
This commit is contained in:
@@ -12,6 +12,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
href?: string;
|
||||
fullWidth?: boolean;
|
||||
openInNewTab?: boolean;
|
||||
iconOnly?: boolean;
|
||||
}
|
||||
|
||||
const Button = ({
|
||||
@@ -24,6 +25,7 @@ const Button = ({
|
||||
href,
|
||||
fullWidth,
|
||||
openInNewTab,
|
||||
iconOnly,
|
||||
...props
|
||||
}: ButtonProps) => {
|
||||
const classes = twMerge(
|
||||
@@ -32,6 +34,15 @@ const Button = ({
|
||||
size === "sm" && "text-xs",
|
||||
size === "lg" && "py-[0.65rem]",
|
||||
fullWidth && "w-full",
|
||||
iconOnly && "p-0",
|
||||
iconOnly &&
|
||||
(size === "xs"
|
||||
? "h-6 w-6"
|
||||
: size === "sm"
|
||||
? "h-8 w-8"
|
||||
: size === "lg"
|
||||
? "h-10 w-10"
|
||||
: "h-9 w-9"),
|
||||
variant === "primary" &&
|
||||
"bg-light-1000 dark:bg-dark-1000 dark:text-dark-50",
|
||||
variant === "secondary" &&
|
||||
@@ -47,19 +58,60 @@ const Button = ({
|
||||
<span className="relative flex items-center justify-center">
|
||||
{isLoading && (
|
||||
<span className="absolute">
|
||||
<LoadingSpinner size={size} />
|
||||
<LoadingSpinner size={size === "xs" ? "sm" : size} />
|
||||
</span>
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center",
|
||||
isLoading ? "invisible" : "visible",
|
||||
)}
|
||||
>
|
||||
{iconLeft && <span className="mr-2">{iconLeft}</span>}
|
||||
{children}
|
||||
{iconRight && <span className="ml-1">{iconRight}</span>}
|
||||
</div>
|
||||
{iconOnly ? (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center",
|
||||
isLoading ? "invisible" : "visible",
|
||||
)}
|
||||
>
|
||||
{iconLeft ?? iconRight}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={twMerge(
|
||||
fullWidth
|
||||
? "grid w-full grid-cols-[auto_1fr_auto] items-center gap-x-2"
|
||||
: "flex items-center",
|
||||
isLoading ? "invisible" : "visible",
|
||||
)}
|
||||
>
|
||||
{fullWidth && !iconLeft && iconRight && (
|
||||
<span className="col-start-1 opacity-0">{iconRight}</span>
|
||||
)}
|
||||
{iconLeft && (
|
||||
<span
|
||||
className={twMerge(
|
||||
fullWidth ? "col-start-1 justify-self-start" : "mr-2",
|
||||
)}
|
||||
>
|
||||
{iconLeft}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={twMerge(
|
||||
fullWidth ? "col-start-2 justify-self-center text-center" : "",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
{iconRight && (
|
||||
<span
|
||||
className={twMerge(
|
||||
fullWidth ? "col-start-3 justify-self-end" : "ml-1",
|
||||
)}
|
||||
>
|
||||
{iconRight}
|
||||
</span>
|
||||
)}
|
||||
{fullWidth && !iconRight && iconLeft && (
|
||||
<span className="col-start-3 opacity-0">{iconLeft}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
|
||||
@@ -2,14 +2,19 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Button } from "@headlessui/react";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { env } from "next-runtime-env";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
import { HiBolt } from "react-icons/hi2";
|
||||
import {
|
||||
TbLayoutSidebarLeftCollapse,
|
||||
TbLayoutSidebarLeftExpand,
|
||||
} from "react-icons/tb";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import type { Subscription } from "@kan/shared/utils";
|
||||
import { hasActiveSubscription } from "@kan/shared/utils";
|
||||
|
||||
import boardsIconDark from "~/assets/boards-dark.json";
|
||||
import boardsIconLight from "~/assets/boards-light.json";
|
||||
import membersIconDark from "~/assets/members-dark.json";
|
||||
@@ -18,9 +23,13 @@ import settingsIconDark from "~/assets/settings-dark.json";
|
||||
import settingsIconLight from "~/assets/settings-light.json";
|
||||
import templatesIconDark from "~/assets/templates-dark.json";
|
||||
import templatesIconLight from "~/assets/templates-light.json";
|
||||
import ButtonComponent from "~/components/Button";
|
||||
import ReactiveButton from "~/components/ReactiveButton";
|
||||
import UserMenu from "~/components/UserMenu";
|
||||
import WorkspaceMenu from "~/components/WorkspaceMenu";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
interface SideNavigationProps {
|
||||
user: UserType;
|
||||
@@ -39,13 +48,23 @@ export default function SideNavigation({
|
||||
onCloseSideNav,
|
||||
}: SideNavigationProps) {
|
||||
const router = useRouter();
|
||||
const { workspace } = useWorkspace();
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [isInitialised, setIsInitialised] = useState(false);
|
||||
const { openModal } = useModal();
|
||||
|
||||
const { data: workspaceData } = api.workspace.byId.useQuery({
|
||||
workspacePublicId: workspace.publicId,
|
||||
});
|
||||
|
||||
const subscriptions = workspaceData?.subscriptions as
|
||||
| Subscription[]
|
||||
| undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const savedState = localStorage.getItem("kan_sidebar-collapsed");
|
||||
if (savedState !== null) {
|
||||
setIsCollapsed(JSON.parse(savedState));
|
||||
setIsCollapsed(Boolean(JSON.parse(savedState)));
|
||||
}
|
||||
setIsInitialised(true);
|
||||
}, []);
|
||||
@@ -61,7 +80,9 @@ export default function SideNavigation({
|
||||
|
||||
const { pathname } = router;
|
||||
|
||||
const { theme, resolvedTheme } = useTheme();
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const isCloudEnv = env("NEXT_PUBLIC_KAN_ENV") === "cloud";
|
||||
|
||||
const isDarkMode = resolvedTheme === "dark";
|
||||
|
||||
@@ -148,7 +169,7 @@ export default function SideNavigation({
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<UserMenu
|
||||
email={user.email ?? ""}
|
||||
imageUrl={user.image ?? undefined}
|
||||
@@ -156,6 +177,33 @@ export default function SideNavigation({
|
||||
isCollapsed={isCollapsed}
|
||||
onCloseSideNav={onCloseSideNav}
|
||||
/>
|
||||
{isCloudEnv &&
|
||||
!hasActiveSubscription(subscriptions, "pro") &&
|
||||
!hasActiveSubscription(subscriptions, "team") && (
|
||||
<div className={twMerge(isCollapsed && "flex justify-center")}>
|
||||
{isCollapsed ? (
|
||||
<ButtonComponent
|
||||
iconLeft={<HiBolt />}
|
||||
variant="secondary"
|
||||
href="/settings/workspace?upgrade=pro"
|
||||
aria-label="Upgrade to Pro"
|
||||
title="Upgrade to Pro"
|
||||
iconOnly
|
||||
onClick={() => openModal("UPGRADE_TO_PRO")}
|
||||
/>
|
||||
) : (
|
||||
<ButtonComponent
|
||||
iconLeft={<HiBolt />}
|
||||
fullWidth
|
||||
variant="secondary"
|
||||
href="/settings/workspace?upgrade=pro"
|
||||
onClick={() => openModal("UPGRADE_TO_PRO")}
|
||||
>
|
||||
{t`Upgrade to Pro`}
|
||||
</ButtonComponent>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</>
|
||||
|
||||
@@ -49,13 +49,14 @@ export default function WorkspaceMenu({
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center justify-start gap-1",
|
||||
isCollapsed && "md:flex-col-reverse md:items-stretch",
|
||||
isCollapsed && "md:flex-col-reverse md:items-center",
|
||||
)}
|
||||
>
|
||||
<Menu.Button
|
||||
className={twMerge(
|
||||
"mb-1 flex h-[34px] min-w-0 flex-1 items-center justify-start rounded-md p-1.5 hover:bg-light-200 dark:hover:bg-dark-200",
|
||||
isCollapsed && "md:mb-1.5 md:h-9 md:w-9 md:flex-none md:p-0",
|
||||
isCollapsed &&
|
||||
"md:mb-1.5 md:h-9 md:w-9 md:flex-none md:justify-center md:p-0",
|
||||
)}
|
||||
title={isCollapsed ? workspace.name : undefined}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user