forked from github/plane
fix: notification card (#1583)
This commit is contained in:
parent
26b18b431b
commit
6eb72507a5
@ -10,7 +10,7 @@ import useToast from "hooks/use-toast";
|
||||
import { CustomMenu, Icon, Tooltip } from "components/ui";
|
||||
|
||||
// helper
|
||||
import { stripHTML, replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { stripHTML, replaceUnderscoreIfSnakeCase, truncateText } from "helpers/string.helper";
|
||||
import {
|
||||
formatDateDistance,
|
||||
render12HourFormatTime,
|
||||
@ -32,7 +32,7 @@ type NotificationCardProps = {
|
||||
|
||||
const snoozeOptions = [
|
||||
{
|
||||
label: "1 days",
|
||||
label: "1 day",
|
||||
value: new Date(new Date().getTime() + 24 * 60 * 60 * 1000),
|
||||
},
|
||||
{
|
||||
@ -79,99 +79,100 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
`/${workspaceSlug}/projects/${notification.project}/issues/${notification.data.issue.id}`
|
||||
);
|
||||
}}
|
||||
className={`group relative py-3 px-6 cursor-pointer ${
|
||||
className={`group w-full flex items-center gap-4 p-3 pl-6 relative cursor-pointer ${
|
||||
notification.read_at === null ? "bg-custom-primary-70/5" : "hover:bg-custom-background-200"
|
||||
}`}
|
||||
>
|
||||
{notification.read_at === null && (
|
||||
<span className="absolute top-1/2 left-2 -translate-y-1/2 w-1.5 h-1.5 bg-custom-primary-100 rounded-full" />
|
||||
)}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<div className="relative w-12 h-12 rounded-full">
|
||||
{notification.triggered_by_details.avatar &&
|
||||
notification.triggered_by_details.avatar !== "" ? (
|
||||
<div className="h-12 w-12 rounded-full">
|
||||
<Image
|
||||
src={notification.triggered_by_details.avatar}
|
||||
alt="Profile Image"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="rounded-full"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-12 h-12 bg-custom-background-100 rounded-full flex justify-center items-center">
|
||||
<span className="text-custom-text-100 font-medium text-lg">
|
||||
{notification.triggered_by_details.first_name[0].toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full space-y-2.5">
|
||||
<div className="text-sm">
|
||||
<span className="font-semibold">
|
||||
{notification.triggered_by_details.first_name}{" "}
|
||||
{notification.triggered_by_details.last_name}{" "}
|
||||
<div className="relative w-12 h-12 rounded-full">
|
||||
{notification.triggered_by_details.avatar &&
|
||||
notification.triggered_by_details.avatar !== "" ? (
|
||||
<div className="h-12 w-12 rounded-full">
|
||||
<Image
|
||||
src={notification.triggered_by_details.avatar}
|
||||
alt="Profile Image"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="rounded-full"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-12 h-12 bg-custom-background-80 rounded-full flex justify-center items-center">
|
||||
<span className="text-custom-text-100 font-medium text-lg">
|
||||
{notification.triggered_by_details.first_name[0].toUpperCase()}
|
||||
</span>
|
||||
{notification.data.issue_activity.field !== "comment" &&
|
||||
notification.data.issue_activity.verb}{" "}
|
||||
{notification.data.issue_activity.field === "comment"
|
||||
? "commented"
|
||||
: notification.data.issue_activity.field === "None"
|
||||
? null
|
||||
: replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)}{" "}
|
||||
{notification.data.issue_activity.field !== "comment" &&
|
||||
notification.data.issue_activity.field !== "None"
|
||||
? "to"
|
||||
: ""}
|
||||
<span className="font-semibold">
|
||||
{" "}
|
||||
{notification.data.issue_activity.field !== "None" ? (
|
||||
notification.data.issue_activity.field !== "comment" ? (
|
||||
notification.data.issue_activity.field === "target_date" ? (
|
||||
renderShortDateWithYearFormat(notification.data.issue_activity.new_value)
|
||||
) : notification.data.issue_activity.field === "attachment" ? (
|
||||
"the issue"
|
||||
) : stripHTML(notification.data.issue_activity.new_value).length > 55 ? (
|
||||
stripHTML(notification.data.issue_activity.new_value).slice(0, 50) + "..."
|
||||
) : (
|
||||
stripHTML(notification.data.issue_activity.new_value)
|
||||
)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2.5 w-full overflow-hidden">
|
||||
<div className="text-sm w-full break-words">
|
||||
<span className="font-semibold">
|
||||
{notification.triggered_by_details.first_name}{" "}
|
||||
{notification.triggered_by_details.last_name}{" "}
|
||||
</span>
|
||||
{notification.data.issue_activity.field !== "comment" &&
|
||||
notification.data.issue_activity.verb}{" "}
|
||||
{notification.data.issue_activity.field === "comment"
|
||||
? "commented"
|
||||
: notification.data.issue_activity.field === "None"
|
||||
? null
|
||||
: replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)}{" "}
|
||||
{notification.data.issue_activity.field !== "comment" &&
|
||||
notification.data.issue_activity.field !== "None"
|
||||
? "to"
|
||||
: ""}
|
||||
<span className="font-semibold">
|
||||
{" "}
|
||||
{notification.data.issue_activity.field !== "None" ? (
|
||||
notification.data.issue_activity.field !== "comment" ? (
|
||||
notification.data.issue_activity.field === "target_date" ? (
|
||||
renderShortDateWithYearFormat(notification.data.issue_activity.new_value)
|
||||
) : notification.data.issue_activity.field === "attachment" ? (
|
||||
"the issue"
|
||||
) : stripHTML(notification.data.issue_activity.new_value).length > 55 ? (
|
||||
stripHTML(notification.data.issue_activity.new_value).slice(0, 50) + "..."
|
||||
) : (
|
||||
<span>
|
||||
{`"`}
|
||||
{notification.data.issue_activity.new_value.length > 55
|
||||
? notification?.data?.issue_activity?.issue_comment?.slice(0, 50) + "..."
|
||||
: notification.data.issue_activity.issue_comment}
|
||||
{`"`}
|
||||
</span>
|
||||
stripHTML(notification.data.issue_activity.new_value)
|
||||
)
|
||||
) : (
|
||||
"the issue and assigned it to you."
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex justify-between text-xs">
|
||||
<p className="truncate inline max-w-lg text-custom-text-300 mr-3">
|
||||
{notification.data.issue.identifier}-{notification.data.issue.sequence_id}{" "}
|
||||
{notification.data.issue.name}
|
||||
</p>
|
||||
{notification.snoozed_till ? (
|
||||
<p className="text-custom-text-300 flex items-center gap-x-1">
|
||||
<Icon iconName="schedule" />
|
||||
<span>
|
||||
Till {renderShortDate(notification.snoozed_till)},{" "}
|
||||
{render12HourFormatTime(notification.snoozed_till)}
|
||||
{`"`}
|
||||
{notification.data.issue_activity.new_value.length > 55
|
||||
? notification?.data?.issue_activity?.issue_comment?.slice(0, 50) + "..."
|
||||
: notification.data.issue_activity.issue_comment}
|
||||
{`"`}
|
||||
</span>
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
<p className="text-custom-text-300">{formatDateDistance(notification.created_at)}</p>
|
||||
"the issue and assigned it to you."
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-2 text-xs">
|
||||
<p className="text-custom-text-300">
|
||||
{truncateText(
|
||||
`${notification.data.issue.identifier}-${notification.data.issue.sequence_id} ${notification.data.issue.name}`,
|
||||
50
|
||||
)}
|
||||
</p>
|
||||
{notification.snoozed_till ? (
|
||||
<p className="text-custom-text-300 flex items-center justify-end gap-x-1 flex-shrink-0">
|
||||
<Icon iconName="schedule" className="!text-base -my-0.5" />
|
||||
<span>
|
||||
Till {renderShortDate(notification.snoozed_till)},{" "}
|
||||
{render12HourFormatTime(notification.snoozed_till)}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-custom-text-300 flex-shrink-0">
|
||||
{formatDateDistance(notification.created_at)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute py-1 gap-x-3 right-3 top-3 hidden group-hover:flex">
|
||||
{[
|
||||
{
|
||||
@ -192,7 +193,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
{
|
||||
id: 2,
|
||||
name: notification.archived_at ? "Unarchive" : "Archive",
|
||||
icon: "archive",
|
||||
icon: notification.archived_at ? "unarchive" : "archive",
|
||||
onClick: () => {
|
||||
markNotificationArchivedStatus(notification.id).then(() => {
|
||||
setToastAlert({
|
||||
@ -213,7 +214,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
item.onClick();
|
||||
}}
|
||||
key={item.id}
|
||||
className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded"
|
||||
className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded outline-none"
|
||||
>
|
||||
<Icon iconName={item.icon} className="h-5 w-5 text-custom-text-300" />
|
||||
</button>
|
||||
|
@ -96,7 +96,7 @@ export const NotificationPopover = () => {
|
||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
||||
isActive
|
||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
} ${sidebarCollapse ? "justify-center" : ""}`}
|
||||
>
|
||||
<Icon iconName="notifications" />
|
||||
@ -282,7 +282,7 @@ export const NotificationPopover = () => {
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="p-5 space-y-4">
|
||||
<Loader className="p-5 space-y-4 overflow-y-auto">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
|
@ -1,33 +1,17 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// headless ui
|
||||
import { Transition } from "@headlessui/react";
|
||||
// services
|
||||
import workspaceService from "services/workspace.service";
|
||||
// hooks
|
||||
import useTheme from "hooks/use-theme";
|
||||
import useUser from "hooks/use-user";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import UpgradeToProModal from "./upgrade-to-pro-modal";
|
||||
// ui
|
||||
import { CircularProgress, Icon } from "components/ui";
|
||||
import { Icon } from "components/ui";
|
||||
// icons
|
||||
import {
|
||||
ArrowLongLeftIcon,
|
||||
ChatBubbleOvalLeftEllipsisIcon,
|
||||
ArrowUpCircleIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ArrowLongLeftIcon, ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
|
||||
import { QuestionMarkCircleIcon, DocumentIcon, DiscordIcon, GithubIcon } from "components/icons";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
const helpOptions = [
|
||||
{
|
||||
@ -58,150 +42,77 @@ export interface WorkspaceHelpSectionProps {
|
||||
}
|
||||
|
||||
export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setSidebarActive }) => {
|
||||
const [alert, setAlert] = useState(false);
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
|
||||
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme();
|
||||
|
||||
useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false));
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const [upgradeModal, setUpgradeModal] = useState(false);
|
||||
|
||||
const { data: workspaceDetails } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
|
||||
workspaceSlug ? () => workspaceService.getWorkspace(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
const issueNumber = workspaceDetails?.total_issues || 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpgradeToProModal
|
||||
isOpen={upgradeModal}
|
||||
onClose={() => setUpgradeModal(false)}
|
||||
user={user}
|
||||
issueNumber={issueNumber}
|
||||
/>
|
||||
{!sidebarCollapse && (alert || issueNumber >= 750) && (
|
||||
<div
|
||||
className={`border-t border-custom-sidebar-border-200 p-4 ${
|
||||
issueNumber >= 750
|
||||
? "bg-red-500/10 text-red-600"
|
||||
: issueNumber >= 500
|
||||
? "bg-yellow-500/10 text-yellow-600"
|
||||
: "text-green-600"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<CircularProgress progress={(issueNumber / 1024) * 100} />
|
||||
<div>Free Plan</div>
|
||||
{issueNumber < 750 && (
|
||||
<div
|
||||
className="ml-auto text-custom-text-200 cursor-pointer"
|
||||
onClick={() => setAlert(false)}
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
</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 gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
|
||||
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 py-2 ${
|
||||
sidebarCollapse ? "flex-col" : ""
|
||||
}`}
|
||||
>
|
||||
{alert || issueNumber >= 750 ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`flex items-center gap-2 rounded-md px-2.5 py-1.5 font-medium outline-none text-sm ${
|
||||
issueNumber >= 750
|
||||
? "bg-red-500/10 text-red-500"
|
||||
: "bg-blue-500/10 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-2 rounded-md px-2.5 py-1.5 font-medium outline-none text-sm ${
|
||||
issueNumber >= 750
|
||||
? "bg-red-500/10 text-red-600"
|
||||
: issueNumber >= 500
|
||||
? "bg-yellow-500/10 text-yellow-600"
|
||||
: "bg-green-500/10 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>
|
||||
{!sidebarCollapse && (
|
||||
<div className="w-1/2 text-center rounded-md px-2.5 py-1.5 font-medium outline-none text-sm bg-green-500/10 text-green-500">
|
||||
Free Plan
|
||||
</div>
|
||||
)}
|
||||
<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" : ""
|
||||
<div
|
||||
className={`w-1/2 flex justify-evenly items-center gap-1 ${
|
||||
sidebarCollapse ? "flex-col" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "h",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
title="Shortcuts"
|
||||
>
|
||||
<Icon iconName="bolt" />
|
||||
</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" />
|
||||
</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
|
||||
type="button"
|
||||
className={`flex items-center gap-x-1 rounded-md p-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" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "h",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
title="Shortcuts"
|
||||
>
|
||||
<Icon iconName="bolt" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex items-center gap-x-1 rounded-md p-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" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-3 rounded-md p-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 p-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>
|
||||
|
||||
<div className="relative">
|
||||
<Transition
|
||||
|
@ -43,7 +43,7 @@ const userLinks = (workspaceSlug: string) => [
|
||||
export const WorkspaceSidebarDropdown = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// fetching user details
|
||||
|
||||
const { user, mutateUser } = useUser();
|
||||
|
||||
const { collapsed: sidebarCollapse } = useThemeHook();
|
||||
@ -139,8 +139,8 @@ export const WorkspaceSidebarDropdown = () => {
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="fixed left-2 z-20 mt-1 flex w-full max-w-[17rem] origin-top-left flex-col rounded-md
|
||||
border border-custom-sidebar-border-200 bg-custom-sidebar-background-90 shadow-lg focus:outline-none"
|
||||
className="fixed left-4 z-20 mt-1 flex flex-col w-full max-w-[17rem] origin-top-left rounded-md
|
||||
border border-custom-sidebar-border-200 bg-custom-sidebar-background-90 shadow-lg outline-none"
|
||||
>
|
||||
<div className="flex flex-col items-start justify-start gap-3 p-3">
|
||||
<div className="text-sm text-custom-sidebar-text-200">{user?.email}</div>
|
||||
|
@ -1,248 +0,0 @@
|
||||
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-200 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;
|
@ -217,7 +217,7 @@ export const render12HourFormatTime = (date: string | Date): string => {
|
||||
if (hours > 12) hours -= 12;
|
||||
}
|
||||
|
||||
return hours + ":" + minutes + " " + period;
|
||||
return hours + ":" + (minutes < 10 ? `0${minutes}` : minutes) + " " + period;
|
||||
};
|
||||
|
||||
export const render24HourFormatTime = (date: string | Date): string => {
|
||||
|
@ -44,28 +44,18 @@ const BillingSettings: NextPage = () => {
|
||||
<section className="space-y-8">
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold leading-6">Billing & Plans</h3>
|
||||
<p className="mt-4 text-sm text-custom-text-200">[Free launch preview] plan Pro</p>
|
||||
<p className="mt-4 text-sm text-custom-text-200">Free launch preview</p>
|
||||
</div>
|
||||
<div className="space-y-8 md:w-2/3">
|
||||
<div>
|
||||
<div className="w-80 rounded-md border border-custom-border-200 bg-custom-background-100 p-4 text-center">
|
||||
<h4 className="text-md mb-1 leading-6">Payment due</h4>
|
||||
<h2 className="text-3xl font-extrabold">--</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-md mb-1 leading-6">Current plan</h4>
|
||||
<p className="mb-3 text-sm text-custom-text-200">
|
||||
You are currently using the free plan
|
||||
</p>
|
||||
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
|
||||
<SecondaryButton outline>View Plans and Upgrade</SecondaryButton>
|
||||
<SecondaryButton outline>View Plans</SecondaryButton>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-md mb-1 leading-6">Billing history</h4>
|
||||
<p className="mb-3 text-sm text-custom-text-200">There are no invoices to display</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user