forked from github/plane
feat: web waitlist modal integration (#1487)
* dev : Updating the limit of the issues in the sidebar and a weight list modal * dev: integrated supabase and implemented web waitlist api endpoint * dev : updated web pro weightlist request * dev: rename typo * dev: web waitlist endpoint update * update: ui fixes * fix: removed supabase from env.example * chore: replaced supabase npm package to cdn * chore: updated supabase req * fix: Handled error status and error message. --------- Co-authored-by: srinivaspendem <you@example.comsrinivaspendem2612@gmail.com> Co-authored-by: gurusainath <gurusainath007@gmail.com>
This commit is contained in:
parent
253edebb93
commit
eba2f3820a
39
apps/app/components/ui/circular-progress.tsx
Normal file
39
apps/app/components/ui/circular-progress.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export const CircularProgress = ({ progress }: { progress: number }) => {
|
||||||
|
const [circumference, setCircumference] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const radius = 40;
|
||||||
|
const calcCircumference = 2 * Math.PI * radius;
|
||||||
|
setCircumference(calcCircumference);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const progressAngle = (progress / 100) * 360 >= 360 ? 359.9 : (progress / 100) * 360;
|
||||||
|
const progressX = 50 + Math.cos((progressAngle - 90) * (Math.PI / 180)) * 40;
|
||||||
|
const progressY = 50 + Math.sin((progressAngle - 90) * (Math.PI / 180)) * 40;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-5 w-5">
|
||||||
|
<svg className="absolute top-0 left-0" viewBox="0 0 100 100">
|
||||||
|
<circle
|
||||||
|
className="stroke-current"
|
||||||
|
cx="50"
|
||||||
|
cy="50"
|
||||||
|
r="40"
|
||||||
|
strokeWidth="12"
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray={`${circumference} ${circumference}`}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="fill-current"
|
||||||
|
d={`M50 10
|
||||||
|
A40 40 0 ${progress > 50 ? 1 : 0} 1 ${progressX} ${progressY}
|
||||||
|
L50 50 Z`}
|
||||||
|
strokeWidth="12"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -26,3 +26,4 @@ export * from "./product-updates-modal";
|
|||||||
export * from "./integration-and-import-export-banner";
|
export * from "./integration-and-import-export-banner";
|
||||||
export * from "./range-datepicker";
|
export * from "./range-datepicker";
|
||||||
export * from "./icon";
|
export * from "./icon";
|
||||||
|
export * from "./circular-progress";
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { useState, useRef, FC } from "react";
|
import { useState, useRef, FC } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Transition } from "@headlessui/react";
|
import { Transition } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
@ -12,8 +16,19 @@ import {
|
|||||||
ArrowLongLeftIcon,
|
ArrowLongLeftIcon,
|
||||||
ChatBubbleOvalLeftEllipsisIcon,
|
ChatBubbleOvalLeftEllipsisIcon,
|
||||||
RocketLaunchIcon,
|
RocketLaunchIcon,
|
||||||
|
ArrowUpCircleIcon,
|
||||||
|
XMarkIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { QuestionMarkCircleIcon, DocumentIcon, DiscordIcon, GithubIcon } from "components/icons";
|
import { QuestionMarkCircleIcon, DocumentIcon, DiscordIcon, GithubIcon } from "components/icons";
|
||||||
|
// services
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
|
||||||
|
// ui
|
||||||
|
import { CircularProgress } from "components/ui";
|
||||||
|
// components
|
||||||
|
import UpgradeToProModal from "./upgrade-to-pro-modal";
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
|
||||||
const helpOptions = [
|
const helpOptions = [
|
||||||
{
|
{
|
||||||
@ -43,7 +58,14 @@ export interface WorkspaceHelpSectionProps {
|
|||||||
setSidebarActive: React.Dispatch<React.SetStateAction<boolean>>;
|
setSidebarActive: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type progress = {
|
||||||
|
progress: number;
|
||||||
|
};
|
||||||
export const WorkspaceHelpSection: FC<WorkspaceHelpSectionProps> = (props) => {
|
export const WorkspaceHelpSection: FC<WorkspaceHelpSectionProps> = (props) => {
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { setSidebarActive } = props;
|
const { setSidebarActive } = props;
|
||||||
// theme
|
// theme
|
||||||
const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme();
|
const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme();
|
||||||
@ -54,105 +76,192 @@ export const WorkspaceHelpSection: FC<WorkspaceHelpSectionProps> = (props) => {
|
|||||||
// hooks
|
// hooks
|
||||||
useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false));
|
useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false));
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
const helpOptionMode = sidebarCollapse ? "left-full" : "left-[-75px]";
|
const helpOptionMode = sidebarCollapse ? "left-full" : "left-[-75px]";
|
||||||
|
|
||||||
return (
|
const [alert, setAlert] = useState(false);
|
||||||
<div
|
|
||||||
className={`flex w-full items-center justify-between self-baseline border-t border-custom-sidebar-border-100 bg-custom-sidebar-background-100 px-6 py-2 ${
|
|
||||||
sidebarCollapse ? "flex-col" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-custom-sidebar-text-200 outline-none hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 ${
|
|
||||||
sidebarCollapse ? "w-full justify-center" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", {
|
|
||||||
key: "h",
|
|
||||||
});
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
title="Shortcuts"
|
|
||||||
>
|
|
||||||
<RocketLaunchIcon className="h-4 w-4 text-custom-sidebar-text-200" />
|
|
||||||
{!sidebarCollapse && <span>Shortcuts</span>}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-custom-sidebar-text-200 outline-none hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 ${
|
|
||||||
sidebarCollapse ? "w-full justify-center" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
|
||||||
title="Help"
|
|
||||||
>
|
|
||||||
<QuestionMarkCircleIcon className="h-4 w-4 text-custom-sidebar-text-200" />
|
|
||||||
{!sidebarCollapse && <span>Help</span>}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-custom-sidebar-text-200 outline-none hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 md:hidden"
|
|
||||||
onClick={() => setSidebarActive(false)}
|
|
||||||
>
|
|
||||||
<ArrowLongLeftIcon className="h-4 w-4 flex-shrink-0 text-custom-sidebar-text-200 group-hover:text-custom-sidebar-text-100" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`hidden items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-custom-sidebar-text-200 outline-none hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 md:flex ${
|
|
||||||
sidebarCollapse ? "w-full justify-center" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => toggleCollapsed()}
|
|
||||||
>
|
|
||||||
<ArrowLongLeftIcon
|
|
||||||
className={`h-4 w-4 flex-shrink-0 text-custom-sidebar-text-200 duration-300 group-hover:text-custom-sidebar-text-100 ${
|
|
||||||
sidebarCollapse ? "rotate-180" : ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="relative">
|
const [upgradeModal, setUpgradeModal] = useState(false);
|
||||||
<Transition
|
|
||||||
show={isNeedHelpOpen}
|
const { data: workspaceDetails } = useSWR(
|
||||||
enter="transition ease-out duration-100"
|
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
|
||||||
enterFrom="transform opacity-0 scale-95"
|
workspaceSlug ? () => workspaceService.getWorkspace(workspaceSlug as string) : null
|
||||||
enterTo="transform opacity-100 scale-100"
|
);
|
||||||
leave="transition ease-in duration-75"
|
const issueNumber = workspaceDetails?.total_issues || 0;
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
return (
|
||||||
>
|
<>
|
||||||
|
<UpgradeToProModal
|
||||||
|
isOpen={upgradeModal}
|
||||||
|
onClose={() => setUpgradeModal(false)}
|
||||||
|
user={user}
|
||||||
|
issueNumber={issueNumber}
|
||||||
|
/>
|
||||||
|
{!sidebarCollapse && (alert || (issueNumber && issueNumber >= 750)) ? (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
className={`absolute bottom-2 ${helpOptionMode} space-y-2 rounded-sm bg-custom-sidebar-background-80 p-1 shadow-md`}
|
className={`border-t p-4 ${
|
||||||
ref={helpOptionsRef}
|
issueNumber >= 750
|
||||||
|
? "bg-red-50 text-red-600 border-red-200"
|
||||||
|
: issueNumber >= 500
|
||||||
|
? "bg-yellow-50 text-yellow-600 border-yellow-200"
|
||||||
|
: "text-green-600"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{helpOptions.map(({ name, Icon, href, onClick }) => {
|
<div className="flex items-center gap-2 w-full">
|
||||||
if (href)
|
<CircularProgress progress={(issueNumber / 1024) * 100} />
|
||||||
return (
|
<div className="">Free Plan</div>
|
||||||
<Link href={href} key={name}>
|
{issueNumber < 750 && (
|
||||||
<a
|
<div className="ml-auto text-custom-text-200" onClick={() => setAlert(false)}>
|
||||||
target="_blank"
|
<XMarkIcon className="h-4 w-4" />
|
||||||
className="flex items-center gap-x-2 whitespace-nowrap rounded-md px-2 py-1 text-xs hover:bg-custom-sidebar-background-90"
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-custom-text-200 text-xs mt-2">
|
||||||
|
This workspace has used {issueNumber} of its 1024 issues creation limit (
|
||||||
|
{((issueNumber / 1024) * 100).toFixed(0)}
|
||||||
|
%).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`flex w-full items-center justify-between self-baseline border-t border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-2 ${
|
||||||
|
sidebarCollapse ? "flex-col" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{alert || (issueNumber && issueNumber >= 750) ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`flex items-center gap-x-1 rounded-md px-2 py-2 font-medium outline-none text-sm
|
||||||
|
${
|
||||||
|
issueNumber >= 750
|
||||||
|
? "bg-custom-primary-100 text-white"
|
||||||
|
: "bg-blue-50 text-custom-primary-100"
|
||||||
|
}
|
||||||
|
${sidebarCollapse ? "w-full justify-center" : ""}`}
|
||||||
|
title="Shortcuts"
|
||||||
|
onClick={() => setUpgradeModal(true)}
|
||||||
|
>
|
||||||
|
<ArrowUpCircleIcon className="h-4 w-4 " />
|
||||||
|
{!sidebarCollapse && <span> Learn more</span>}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`flex items-center gap-x-1 rounded-md px-2 py-2 font-medium outline-none text-sm ${
|
||||||
|
issueNumber >= 750
|
||||||
|
? "bg-red-50 text-red-600"
|
||||||
|
: issueNumber >= 500
|
||||||
|
? "bg-yellow-50 text-yellow-600"
|
||||||
|
: "bg-green-50 text-green-600"
|
||||||
|
}
|
||||||
|
${sidebarCollapse ? "w-full justify-center" : ""}`}
|
||||||
|
title="Shortcuts"
|
||||||
|
onClick={() => setAlert(true)}
|
||||||
|
>
|
||||||
|
<CircularProgress
|
||||||
|
progress={(issueNumber / 1024) * 100 > 100 ? 100 : (issueNumber / 1024) * 100}
|
||||||
|
/>
|
||||||
|
{!sidebarCollapse && <span>Free Plan</span>}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||||
|
sidebarCollapse ? "w-full justify-center" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", {
|
||||||
|
key: "h",
|
||||||
|
});
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
title="Shortcuts"
|
||||||
|
>
|
||||||
|
<RocketLaunchIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
|
{/* {!sidebarCollapse && <span>Shortcuts</span>} */}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||||
|
sidebarCollapse ? "w-full justify-center" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||||
|
title="Help"
|
||||||
|
>
|
||||||
|
<QuestionMarkCircleIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
|
{/* {!sidebarCollapse && <span>Help</span>} */}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:hidden"
|
||||||
|
onClick={() => setSidebarActive(false)}
|
||||||
|
>
|
||||||
|
<ArrowLongLeftIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200 group-hover:text-custom-text-100" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`hidden items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:flex ${
|
||||||
|
sidebarCollapse ? "w-full justify-center" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => toggleCollapsed()}
|
||||||
|
>
|
||||||
|
<ArrowLongLeftIcon
|
||||||
|
className={`h-4 w-4 flex-shrink-0 text-custom-text-200 duration-300 group-hover:text-custom-text-100 ${
|
||||||
|
sidebarCollapse ? "rotate-180" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<Transition
|
||||||
|
show={isNeedHelpOpen}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`absolute bottom-2 ${helpOptionMode} space-y-2 rounded-sm bg-custom-background-80 p-1 shadow-md`}
|
||||||
|
ref={helpOptionsRef}
|
||||||
|
>
|
||||||
|
{helpOptions.map(({ name, Icon, href, onClick }) => {
|
||||||
|
if (href)
|
||||||
|
return (
|
||||||
|
<Link href={href} key={name}>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
className="flex items-center gap-x-2 whitespace-nowrap rounded-md px-2 py-1 text-xs hover:bg-custom-background-90"
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4 text-custom-text-200" />
|
||||||
|
<span className="text-sm">{name}</span>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={name}
|
||||||
|
type="button"
|
||||||
|
onClick={onClick ? onClick : undefined}
|
||||||
|
className="flex w-full items-center gap-x-2 whitespace-nowrap rounded-md px-2 py-1 text-xs hover:bg-custom-background-90"
|
||||||
>
|
>
|
||||||
<Icon className="h-4 w-4 text-custom-sidebar-text-200" />
|
<Icon className="h-4 w-4 text-custom-sidebar-text-200" />
|
||||||
<span className="text-sm">{name}</span>
|
<span className="text-sm">{name}</span>
|
||||||
</a>
|
</button>
|
||||||
</Link>
|
);
|
||||||
);
|
})}
|
||||||
else
|
</div>
|
||||||
return (
|
</Transition>
|
||||||
<button
|
</div>
|
||||||
key={name}
|
|
||||||
type="button"
|
|
||||||
onClick={onClick ? onClick : undefined}
|
|
||||||
className="flex w-full items-center gap-x-2 whitespace-nowrap rounded-md px-2 py-1 text-xs hover:bg-custom-sidebar-background-90"
|
|
||||||
>
|
|
||||||
<Icon className="h-4 w-4 text-custom-sidebar-text-200" />
|
|
||||||
<span className="text-sm">{name}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
248
apps/app/components/workspace/upgrade-to-pro-modal.tsx
Normal file
248
apps/app/components/workspace/upgrade-to-pro-modal.tsx
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// icons
|
||||||
|
import { XCircleIcon, RocketLaunchIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||||
|
// ui
|
||||||
|
import { CircularProgress } from "components/ui";
|
||||||
|
// types
|
||||||
|
import type { ICurrentUserResponse, IWorkspace } from "types";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
supabase: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
user: ICurrentUserResponse | undefined;
|
||||||
|
issueNumber: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpgradeToProModal: React.FC<Props> = ({ isOpen, onClose, user, issueNumber }) => {
|
||||||
|
const [supabaseClient, setSupabaseClient] = useState<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Create a Supabase client
|
||||||
|
if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
|
||||||
|
const { createClient } = window.supabase;
|
||||||
|
const supabase = createClient(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
||||||
|
{
|
||||||
|
auth: {
|
||||||
|
autoRefreshToken: false,
|
||||||
|
persistSession: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (supabase) {
|
||||||
|
setSupabaseClient(supabase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const proFeatures = [
|
||||||
|
"Everything in free",
|
||||||
|
"Unlimited users",
|
||||||
|
"Unlimited file uploads",
|
||||||
|
"Priority Support",
|
||||||
|
"Custom Theming",
|
||||||
|
"Access to Roadmap",
|
||||||
|
"Plane AI (GPT unlimited)",
|
||||||
|
];
|
||||||
|
|
||||||
|
const [errorMessage, setErrorMessage] = useState<null | { status: String; message: string }>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [loader, setLoader] = useState(false);
|
||||||
|
const submitEmail = async () => {
|
||||||
|
setLoader(true);
|
||||||
|
const payload = { email: user?.email || "" };
|
||||||
|
|
||||||
|
if (supabaseClient) {
|
||||||
|
if (payload?.email) {
|
||||||
|
const emailExists = await supabaseClient
|
||||||
|
.from("web-waitlist")
|
||||||
|
.select("id,email,count")
|
||||||
|
.eq("email", payload?.email);
|
||||||
|
if (emailExists.data.length === 0) {
|
||||||
|
const emailCreation = await supabaseClient
|
||||||
|
.from("web-waitlist")
|
||||||
|
.insert([{ email: payload?.email, count: 1, last_visited: new Date() }])
|
||||||
|
.select("id,email,count");
|
||||||
|
if (emailCreation.status === 201)
|
||||||
|
setErrorMessage({ status: "success", message: "Successfully registered." });
|
||||||
|
else setErrorMessage({ status: "insert_error", message: "Insertion Error." });
|
||||||
|
} else {
|
||||||
|
const emailCountUpdate = await supabaseClient
|
||||||
|
.from("web-waitlist")
|
||||||
|
.upsert({
|
||||||
|
id: emailExists.data[0]?.id,
|
||||||
|
count: emailExists.data[0]?.count + 1,
|
||||||
|
last_visited: new Date(),
|
||||||
|
})
|
||||||
|
.select("id,email,count");
|
||||||
|
if (emailCountUpdate.status === 201)
|
||||||
|
setErrorMessage({
|
||||||
|
status: "email_already_exists",
|
||||||
|
message: "Email already exists.",
|
||||||
|
});
|
||||||
|
else setErrorMessage({ status: "update_error", message: "Update Error." });
|
||||||
|
}
|
||||||
|
} else setErrorMessage({ status: "email_required", message: "Please provide email." });
|
||||||
|
} else
|
||||||
|
setErrorMessage({
|
||||||
|
status: "supabase_error",
|
||||||
|
message: "Network error. Please try again later.",
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoader(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-100 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-4xl">
|
||||||
|
<div className="flex flex-wrap">
|
||||||
|
<div className="w-full md:w-3/5 p-6 flex flex-col gap-y-6">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div
|
||||||
|
className={`font-semibold outline-none text-sm mt-1.5 ${
|
||||||
|
issueNumber >= 750
|
||||||
|
? "text-red-600"
|
||||||
|
: issueNumber >= 500
|
||||||
|
? "text-yellow-600"
|
||||||
|
: "text-green-600"
|
||||||
|
}`}
|
||||||
|
title="Shortcuts"
|
||||||
|
>
|
||||||
|
<CircularProgress
|
||||||
|
progress={
|
||||||
|
(issueNumber / 1024) * 100 > 100 ? 100 : (issueNumber / 1024) * 100
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<div className="font-semibold text-lg">Upgrade to pro</div>
|
||||||
|
<div className="text-custom-text-200 text-sm">
|
||||||
|
This workspace has used {issueNumber} of its 1024 issues creation limit (
|
||||||
|
{((issueNumber / 1024) * 100).toFixed(2)}%).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={handleClose}
|
||||||
|
className="w-5 h-5 text-custom-text-200 cursor-pointer mt-1.5 md:hidden block ml-auto"
|
||||||
|
>
|
||||||
|
<XCircleIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 pt-6">
|
||||||
|
<div
|
||||||
|
className={`font-semibold outline-none text-sm mt-1.5 w-5 h-5 text-[#892FFF] flex-shrink-0`}
|
||||||
|
title="Shortcuts"
|
||||||
|
>
|
||||||
|
<RocketLaunchIcon />
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<div className="font-semibold text-lg">Order summary</div>
|
||||||
|
<div className="text-custom-text-200 text-sm">
|
||||||
|
Priority support, file uploads, and access to premium features.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap my-4">
|
||||||
|
{proFeatures.map((feature, index) => (
|
||||||
|
<div key={index} className="w-1/2 py-2 flex gap-2 my-1.5">
|
||||||
|
<div className="w-5 h-5 mt-0.5 text-green-600">
|
||||||
|
<CheckCircleIcon />
|
||||||
|
</div>
|
||||||
|
<div>{feature}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full md:w-2/5 bg-custom-background-90 p-6 flex flex-col">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="font-semibold text-lg">Summary</div>
|
||||||
|
<div
|
||||||
|
onClick={handleClose}
|
||||||
|
className="w-5 h-5 text-custom-text-200 cursor-pointer mt-1.5 hidden md:block"
|
||||||
|
>
|
||||||
|
<XCircleIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-custom-text-200 text-sm mt-4">
|
||||||
|
Plane application is currently in dev-mode. We will soon introduce Pro plans
|
||||||
|
once general availability has been established. Stay tuned for more updates.
|
||||||
|
In the meantime, Plane remains free and unrestricted.
|
||||||
|
<br /> <br />
|
||||||
|
We{"'"}ll ensure a smooth transition from the community version to the Pro
|
||||||
|
plan for you.
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
disabled={loader}
|
||||||
|
onClick={() => submitEmail()}
|
||||||
|
type="button"
|
||||||
|
className="mt-5 md:mt-auto whitespace-nowrap max-w-min items-center gap-x-1 rounded-md px-3 py-2 font-medium outline-none text-sm bg-custom-primary-100 text-white"
|
||||||
|
>
|
||||||
|
{loader ? "Loading.." : " Join waitlist"}
|
||||||
|
</button>
|
||||||
|
{errorMessage && (
|
||||||
|
<div
|
||||||
|
className={`mt-1 text-sm ${
|
||||||
|
errorMessage && errorMessage?.status === "success"
|
||||||
|
? "text-green-500"
|
||||||
|
: " text-red-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{errorMessage?.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpgradeToProModal;
|
@ -13,6 +13,7 @@ class MyDocument extends Document {
|
|||||||
<link rel="apple-touch-icon" href="/icon.png" />
|
<link rel="apple-touch-icon" href="/icon.png" />
|
||||||
<meta name="theme-color" content="#fff" />
|
<meta name="theme-color" content="#fff" />
|
||||||
<script defer data-domain="app.plane.so" src="https://plausible.io/js/script.js" />
|
<script defer data-domain="app.plane.so" src="https://plausible.io/js/script.js" />
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2" />
|
||||||
{isSessionRecorderEnabled && process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY && (
|
{isSessionRecorderEnabled && process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY && (
|
||||||
<script
|
<script
|
||||||
defer
|
defer
|
||||||
|
22
apps/app/services/web-waitlist.service.ts
Normal file
22
apps/app/services/web-waitlist.service.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// services
|
||||||
|
import APIService from "services/api.service";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { IWebWaitListResponse } from "types";
|
||||||
|
|
||||||
|
class WebWailtListServices extends APIService {
|
||||||
|
constructor() {
|
||||||
|
const origin = typeof window !== "undefined" ? window.location.origin || "" : "";
|
||||||
|
super(origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create({ email }: { email: string }): Promise<IWebWaitListResponse> {
|
||||||
|
return this.post(`/api/web-waitlist`, { email: email })
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new WebWailtListServices();
|
1
apps/app/types/index.d.ts
vendored
1
apps/app/types/index.d.ts
vendored
@ -15,6 +15,7 @@ export * from "./importer";
|
|||||||
export * from "./inbox";
|
export * from "./inbox";
|
||||||
export * from "./analytics";
|
export * from "./analytics";
|
||||||
export * from "./calendar";
|
export * from "./calendar";
|
||||||
|
export * from "./waitlist";
|
||||||
|
|
||||||
export type NestedKeyOf<ObjectType extends object> = {
|
export type NestedKeyOf<ObjectType extends object> = {
|
||||||
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
|
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
|
||||||
|
3
apps/app/types/waitlist.d.ts
vendored
Normal file
3
apps/app/types/waitlist.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface IWebWaitListResponse {
|
||||||
|
status: string;
|
||||||
|
}
|
1
apps/app/types/workspace.d.ts
vendored
1
apps/app/types/workspace.d.ts
vendored
@ -14,6 +14,7 @@ export interface IWorkspace {
|
|||||||
readonly created_by: string;
|
readonly created_by: string;
|
||||||
readonly updated_by: string;
|
readonly updated_by: string;
|
||||||
company_size: number | null;
|
company_size: number | null;
|
||||||
|
total_issues: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkspaceLite {
|
export interface IWorkspaceLite {
|
||||||
|
@ -20,7 +20,9 @@
|
|||||||
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
|
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
|
||||||
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
|
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
|
||||||
"NEXT_PUBLIC_SLACK_CLIENT_ID",
|
"NEXT_PUBLIC_SLACK_CLIENT_ID",
|
||||||
"NEXT_PUBLIC_SLACK_CLIENT_SECRET"
|
"NEXT_PUBLIC_SLACK_CLIENT_SECRET",
|
||||||
|
"NEXT_PUBLIC_SUPABASE_URL",
|
||||||
|
"NEXT_PUBLIC_SUPABASE_ANON_KEY"
|
||||||
],
|
],
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"build": {
|
"build": {
|
||||||
|
Loading…
Reference in New Issue
Block a user