Merge branch 'chore-admin-file-structure' of gurusainath:makeplane/plane into chore-admin-file-structure

This commit is contained in:
guru_sainath 2024-05-06 11:54:58 +05:30
commit 98a770c9df
109 changed files with 1282 additions and 1477 deletions

2
admin/.env.example Normal file
View File

@ -0,0 +1,2 @@
NEXT_PUBLIC_APP_URL=
NEXT_PUBLIC_API_BASE_URL=

View File

@ -5,6 +5,8 @@ import { ThemeProvider } from "next-themes";
// lib
import { StoreProvider } from "@/lib/store-context";
import { AppWrapper } from "@/lib/wrappers";
// constants
import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_USER_NAME, SITE_KEYWORDS, SITE_TITLE } from "@/constants/seo";
// styles
import "./globals.css";
@ -12,16 +14,35 @@ interface RootLayoutProps {
children: ReactNode;
}
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => (
<html lang="en">
<body className={`antialiased`}>
<StoreProvider {...pageProps}>
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
<AppWrapper>{children}</AppWrapper>
</ThemeProvider>
</StoreProvider>
</body>
</html>
);
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => {
const prefix = parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0") === 0 ? "/" : "/god-mode/";
return (
<html lang="en">
<head>
<title>{SITE_TITLE}</title>
<meta property="og:site_name" content={SITE_NAME} />
<meta property="og:title" content={SITE_TITLE} />
<meta property="og:url" content={SITE_URL} />
<meta name="description" content={SITE_DESCRIPTION} />
<meta property="og:description" content={SITE_DESCRIPTION} />
<meta name="keywords" content={SITE_KEYWORDS} />
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
<link rel="apple-touch-icon" sizes="180x180" href={`${prefix}favicon/apple-touch-icon.png`} />
<link rel="icon" type="image/png" sizes="32x32" href={`${prefix}favicon/favicon-32x32.png`} />
<link rel="icon" type="image/png" sizes="16x16" href={`${prefix}favicon/favicon-16x16.png`} />
<link rel="manifest" href={`${prefix}site.webmanifest.json`} />
<link rel="shortcut icon" href={`${prefix}favicon/favicon.ico`} />
</head>
<body className={`antialiased`}>
<StoreProvider {...pageProps}>
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
<AppWrapper>{children}</AppWrapper>
</ThemeProvider>
</StoreProvider>
</body>
</html>
);
};
export default RootLayout;

View File

@ -5,13 +5,13 @@ import { useSearchParams } from "next/navigation";
// services
import { AuthService } from "@/services/auth.service";
// ui
import { Button, Input } from "@plane/ui";
import { Button, Input, Spinner } from "@plane/ui";
// components
import { Banner } from "components/common";
// icons
import { Eye, EyeOff } from "lucide-react";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper";
import { API_BASE_URL } from "@/helpers/common.helper";
// service initialization
const authService = new AuthService();
@ -52,6 +52,7 @@ export const InstanceSignInForm: FC = (props) => {
const [showPassword, setShowPassword] = useState(false);
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [formData, setFormData] = useState<TFormData>(defaultFromData);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
setFormData((prev) => ({ ...prev, [key]: value }));
@ -85,27 +86,40 @@ export const InstanceSignInForm: FC = (props) => {
} else return { type: undefined, message: undefined };
}, [errorCode, errorMessage]);
const isButtonDisabled = useMemo(() => (formData.email && formData.password ? false : true), [formData]);
const isButtonDisabled = useMemo(
() => (!isSubmitting && formData.email && formData.password ? false : true),
[formData.email, formData.password, isSubmitting]
);
return (
<div className="relative w-full h-full overflow-hidden container mx-auto px-5 md:px-0 flex justify-center items-center">
<div className="w-full md:w-4/6 lg:w-3/6 xl:w-2/6 space-y-10">
<div className="relative w-full h-full overflow-hidden container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10 flex flex-col justify-center items-center">
<div className="relative flex flex-col space-y-6">
<div className="text-center space-y-1">
<h3 className="text-3xl font-bold">Manage your Plane instance</h3>
<p className="font-medium text-custom-text-400">Configure instance-wide settings to secure your instance</p>
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Manage your Plane instance
</h3>
<p className="font-medium text-onboarding-text-400">
Configure instance-wide settings to secure your instance
</p>
</div>
{errorData.type && errorData?.message && <Banner type="error" message={errorData?.message} />}
<form className="space-y-4" method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-in/`}>
<form
className="space-y-4"
method="POST"
action={`${API_BASE_URL}/api/instances/admins/sign-in/`}
onSubmit={() => setIsSubmitting(true)}
onError={() => setIsSubmitting(false)}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email <span className="text-red-500">*</span>
</label>
<Input
className="w-full"
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="email"
name="email"
type="email"
@ -118,12 +132,12 @@ export const InstanceSignInForm: FC = (props) => {
</div>
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
Password <span className="text-red-500">*</span>
</label>
<div className="relative">
<Input
className={cn("w-full pr-10")}
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="password"
name="password"
type={showPassword ? "text" : "password"}
@ -153,7 +167,7 @@ export const InstanceSignInForm: FC = (props) => {
</div>
<div className="py-2">
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
Sign in
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Sign in"}
</Button>
</div>
</form>

View File

@ -5,13 +5,13 @@ import { useSearchParams } from "next/navigation";
// services
import { AuthService } from "@/services/auth.service";
// ui
import { Button, Checkbox, Input } from "@plane/ui";
import { Button, Checkbox, Input, Spinner } from "@plane/ui";
// components
import { Banner, PasswordStrengthMeter } from "components/common";
// icons
import { Eye, EyeOff } from "lucide-react";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper";
import { API_BASE_URL } from "@/helpers/common.helper";
import { getPasswordStrength } from "@/helpers/password.helper";
// service initialization
@ -68,6 +68,7 @@ export const InstanceSignUpForm: FC = (props) => {
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [formData, setFormData] = useState<TFormData>(defaultFromData);
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
setFormData((prev) => ({ ...prev, [key]: value }));
@ -109,6 +110,7 @@ export const InstanceSignUpForm: FC = (props) => {
const isButtonDisabled = useMemo(
() =>
!isSubmitting &&
formData.first_name &&
formData.email &&
formData.password &&
@ -116,15 +118,19 @@ export const InstanceSignUpForm: FC = (props) => {
formData.password === formData.confirm_password
? false
: true,
[formData]
[formData.confirm_password, formData.email, formData.first_name, formData.password, isSubmitting]
);
return (
<div className="relative w-full min-h-full h-auto overflow-hidden container mx-auto px-5 lg:px-0 flex justify-center items-center">
<div className="w-full md:w-4/6 lg:w-3/6 xl:w-2/6 space-y-10">
<div className="relative w-full h-full overflow-hidden container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 flex flex-col justify-center items-center">
<div className="relative flex flex-col space-y-6">
<div className="text-center space-y-1">
<h3 className="text-3xl font-bold">Setup your Plane Instance</h3>
<p className="font-medium text-custom-text-400">Post setup you will be able to manage this Plane instance.</p>
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Setup your Plane Instance
</h3>
<p className="font-medium text-onboarding-text-400">
Post setup you will be able to manage this Plane instance.
</p>
</div>
{errorData.type &&
@ -133,16 +139,22 @@ export const InstanceSignUpForm: FC = (props) => {
<Banner type="error" message={errorData?.message} />
)}
<form className="space-y-4" method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-up/`}>
<form
className="space-y-4"
method="POST"
action={`${API_BASE_URL}/api/instances/admins/sign-up/`}
onSubmit={() => setIsSubmitting(true)}
onError={() => setIsSubmitting(false)}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="flex items-center gap-4">
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="first_name">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="first_name">
First name <span className="text-red-500">*</span>
</label>
<Input
className="w-full"
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="first_name"
name="first_name"
type="text"
@ -154,11 +166,11 @@ export const InstanceSignUpForm: FC = (props) => {
/>
</div>
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="last_name">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="last_name">
Last name
</label>
<Input
className="w-full"
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="last_name"
name="last_name"
type="text"
@ -171,11 +183,11 @@ export const InstanceSignUpForm: FC = (props) => {
</div>
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email <span className="text-red-500">*</span>
</label>
<Input
className="w-full"
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="email"
name="email"
type="email"
@ -191,11 +203,11 @@ export const InstanceSignUpForm: FC = (props) => {
</div>
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="company_name">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="company_name">
Company name
</label>
<Input
className="w-full"
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="company_name"
name="company_name"
type="text"
@ -207,12 +219,12 @@ export const InstanceSignUpForm: FC = (props) => {
</div>
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
Set a password <span className="text-red-500">*</span>
</label>
<div className="relative">
<Input
className={cn("w-full pr-10")}
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="password"
name="password"
type={showPassword ? "text" : "password"}
@ -252,25 +264,33 @@ export const InstanceSignUpForm: FC = (props) => {
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
Confirm password
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
id="confirm_password"
name="confirm_password"
inputSize="md"
value={formData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
<button
type="button"
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(false)}
/>
>
<EyeOff className="h-4 w-4" />
</button>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
<button
type="button"
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(true)}
/>
>
<Eye className="h-4 w-4" />
</button>
)}
</div>
{!!formData.confirm_password && formData.password !== formData.confirm_password && (
@ -288,7 +308,10 @@ export const InstanceSignUpForm: FC = (props) => {
checked={formData.is_telemetry_enabled}
/>
</div>
<label className="text-sm text-custom-text-300 font-medium cursor-pointer" htmlFor="is_telemetry_enabled">
<label
className="text-sm text-onboarding-text-300 font-medium cursor-pointer"
htmlFor="is_telemetry_enabled"
>
Allow Plane to anonymously collect usage events.
</label>
<a href="https://docs.plane.so/telemetry" className="text-sm font-medium text-blue-500 hover:text-blue-600">
@ -298,7 +321,7 @@ export const InstanceSignUpForm: FC = (props) => {
<div className="py-2">
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
Continue
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
</div>
</form>

View File

@ -3,11 +3,10 @@
import { FC, useState, useRef } from "react";
import { Transition } from "@headlessui/react";
import Link from "next/link";
import { FileText, HelpCircle, MoveLeft } from "lucide-react";
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { DiscordIcon, GithubIcon } from "@plane/ui";
// hooks
import { useTheme } from "@/hooks";
// icons
import { DiscordIcon, GithubIcon } from "@plane/ui";
// assets
import packageJson from "package.json";
@ -37,18 +36,26 @@ export const HelpSection: FC = () => {
// refs
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `${process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? `/god-mode/` : `/`}`}`;
return (
<div
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
isSidebarCollapsed ? "flex-col" : ""
}`}
>
<div
className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full justify-end"}`}
>
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
<a
href={redirectionLink}
className={`relative px-2 py-1.5 flex items-center gap-2 font-medium rounded border border-custom-primary-100/20 bg-custom-primary-100/10 text-xs text-custom-primary-200 whitespace-nowrap`}
>
<ExternalLink size={14} />
{!isSidebarCollapsed && "Redirect to plane"}
</a>
<button
type="button"
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
className={`ml-auto grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}

View File

@ -1,15 +1,20 @@
"use client";
import { Fragment } from "react";
import { Fragment, useEffect, useState } from "react";
import { useTheme as useNextTheme } from "next-themes";
import { observer } from "mobx-react-lite";
import { LogOut, UserCog2, Palette } from "lucide-react";
import { Menu, Transition } from "@headlessui/react";
import { Avatar, TOAST_TYPE, setToast } from "@plane/ui";
import { Avatar } from "@plane/ui";
// hooks
import { useTheme, useUser } from "@/hooks";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { AuthService } from "@/services";
// service initialization
const authService = new AuthService();
export const SidebarDropdown = observer(() => {
// store hooks
@ -17,22 +22,21 @@ export const SidebarDropdown = observer(() => {
const { currentUser, signOut } = useUser();
// hooks
const { resolvedTheme, setTheme } = useNextTheme();
const handleSignOut = async () => {
await signOut().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Failed to sign out. Please try again.",
})
);
};
// state
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const handleThemeSwitch = () => {
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
setTheme(newTheme);
};
const handleSignOut = () => signOut();
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]);
return (
<div className="flex max-h-[3.75rem] items-center gap-x-5 gap-y-2 border-b border-custom-sidebar-border-200 px-4 py-3.5">
<div className="h-full w-full truncate">
@ -93,12 +97,12 @@ export const SidebarDropdown = observer(() => {
</Menu.Item>
</div>
<div className="py-2">
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`}>
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`} onSubmit={handleSignOut}>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<Menu.Item
as="button"
type="button"
type="submit"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
onClick={handleSignOut}
>
<LogOut className="h-4 w-4 stroke-[1.5]" />
Sign out

View File

@ -1,4 +1,4 @@
import Head from "next/head";
"use client";
type TPageHeader = {
title?: string;
@ -9,9 +9,9 @@ export const PageHeader: React.FC<TPageHeader> = (props) => {
const { title = "God Mode - Plane", description = "Plane god mode" } = props;
return (
<Head>
<>
<title>{title}</title>
<meta name="description" content={description} />
</Head>
</>
);
};

View File

@ -11,9 +11,9 @@ export const InstanceNotReady: FC = () => (
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
<h1 className="text-3xl font-bold">Welcome aboard Plane!</h1>
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
<Image src={PlaneTakeOffImage} alt="Plane Logo" />
<p className="font-medium text-base text-custom-text-400">
<p className="font-medium text-base text-onboarding-text-400">
Get started by setting up your instance and workspace
</p>
</div>

View File

@ -2,9 +2,8 @@
import React from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import Image from "next/image";
import { useTheme } from "next-themes";
import { useTheme as nextUseTheme } from "next-themes";
// ui
import { Button, getButtonStyling } from "@plane/ui";
// helpers
@ -12,25 +11,19 @@ import { resolveGeneralTheme } from "helpers/common.helper";
// icons
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
import { useTheme } from "@/hooks";
type Props = {
isOpen: boolean;
onClose?: () => void;
};
export const CreateWorkspacePopup: React.FC<Props> = observer((props) => {
const { isOpen, onClose } = props;
export const NewUserPopup: React.FC = observer(() => {
// hooks
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
// theme
const { resolvedTheme } = useTheme();
const { resolvedTheme } = nextUseTheme();
const handleClose = () => {
onClose && onClose();
};
if (!isOpen) return null;
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `${process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? `/god-mode/` : `/`}`}`;
if (!isNewUserPopup) return <></>;
return (
<div className="absolute bottom-8 right-6 p-6 w-96 border border-custom-border-100 shadow-md rounded-xl bg-custom-background-100 z-20">
<div className="absolute bottom-8 right-8 p-6 w-96 border border-custom-border-100 shadow-md rounded-lg bg-custom-background-100">
<div className="flex gap-4">
<div className="grow">
<div className="text-base font-semibold">Create workspace</div>
@ -39,10 +32,10 @@ export const CreateWorkspacePopup: React.FC<Props> = observer((props) => {
workspace, you will need to login again.
</div>
<div className="flex items-center gap-4 pt-2">
<Link href="/create-workspace" className={getButtonStyling("primary", "sm")}>
<a href={redirectionLink} className={getButtonStyling("primary", "sm")}>
Create workspace
</Link>
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
</a>
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
Close
</Button>
</div>

8
admin/constants/seo.ts Normal file
View File

@ -0,0 +1,8 @@
export const SITE_NAME = "Plane | Simple, extensible, open-source project management tool.";
export const SITE_TITLE = "Plane | Simple, extensible, open-source project management tool.";
export const SITE_DESCRIPTION =
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.";
export const SITE_KEYWORDS =
"software development, plan, ship, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration";
export const SITE_URL = "https://app.plane.so/";
export const TWITTER_USER_NAME = "Plane | Simple, extensible, open-source project management tool.";

View File

@ -1,6 +1,8 @@
import { FC, ReactNode } from "react";
// components
import { InstanceSidebar } from "@/components/admin-sidebar";
import { InstanceHeader } from "@/components/auth-header";
import { NewUserPopup } from "@/components/new-user-popup";
type TAdminLayout = {
children: ReactNode;
@ -16,6 +18,7 @@ export const AdminLayout: FC<TAdminLayout> = (props) => {
<InstanceHeader />
<div className="h-full w-full overflow-hidden">{children}</div>
</main>
<NewUserPopup />
</div>
);
};

View File

@ -2,25 +2,42 @@
import { FC, ReactNode } from "react";
import Image from "next/image";
// logo
import { useTheme } from "next-themes";
// logo/ images
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
type TDefaultLayout = {
children: ReactNode;
withoutBackground?: boolean;
};
export const DefaultLayout: FC<TDefaultLayout> = (props) => {
const { children } = props;
const { children, withoutBackground = false } = props;
// hooks
const { resolvedTheme } = useTheme();
return (
<div className="h-screen w-full overflow-hidden overflow-y-auto flex flex-col">
<div className="container h-[100px] flex-shrink-0 mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
<div className="flex items-center gap-x-2">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
<div className="relative">
<div className="h-screen w-full overflow-hidden overflow-y-auto flex flex-col">
<div className="container h-[110px] flex-shrink-0 mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
<div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
</div>
{!withoutBackground && (
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-screen h-full object-cover"
alt="Plane background pattern"
/>
</div>
)}
<div className="relative z-10 mb-[110px] flex-grow">{children}</div>
</div>
<div className="w-full px-5 lg:px-0 mb-[100px] flex-grow">{children}</div>
</div>
);
};

View File

@ -46,7 +46,7 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
if (instance?.instance?.is_setup_done === false && authEnabled === "1")
return (
<DefaultLayout>
<DefaultLayout withoutBackground>
<InstanceNotReady />
</DefaultLayout>
);

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -1 +1,11 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{
"name": "",
"short_name": "",
"icons": [
{ "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@ -0,0 +1,13 @@
{
"name": "Plane God Mode",
"short_name": "Plane God Mode",
"description": "Plane helps you plan your issues, cycles, and product modules.",
"start_url": ".",
"display": "standalone",
"background_color": "#f9fafb",
"theme_color": "#3f76ff",
"icons": [
{ "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
]
}

View File

@ -19,27 +19,4 @@ export class AuthService extends APIService {
throw error;
});
}
async signOut(baseUrl: string): Promise<any> {
await this.requestCSRFToken().then((data) => {
const csrfToken = data?.csrf_token;
if (!csrfToken) throw Error("CSRF token not found");
var form = document.createElement("form");
var element1 = document.createElement("input");
form.method = "POST";
form.action = `${baseUrl}/api/instances/admins/sign-out/`;
element1.value = csrfToken;
element1.name = "csrfmiddlewaretoken";
element1.type = "hidden";
form.appendChild(element1);
document.body.appendChild(form);
form.submit();
});
}
}

View File

@ -75,6 +75,8 @@ export class InstanceStore implements IInstanceStore {
try {
if (this.instance === undefined) this.isLoading = true;
const instance = await this.instanceService.getInstanceInfo();
// handling the new user popup toggle
if (this.instance === undefined && !instance?.instance?.workspaces_exist) this.store.theme.toggleNewUserPopup();
runInAction(() => {
this.isLoading = false;
this.instance = instance;

View File

@ -5,31 +5,41 @@ import { RootStore } from "@/store/root-store";
type TTheme = "dark" | "light";
export interface IThemeStore {
// observables
isNewUserPopup: boolean;
theme: string | undefined;
isSidebarCollapsed: boolean | undefined;
// actions
toggleNewUserPopup: () => void;
toggleSidebar: (collapsed: boolean) => void;
setTheme: (currentTheme: TTheme) => void;
}
export class ThemeStore implements IThemeStore {
// observables
isNewUserPopup: boolean = false;
isSidebarCollapsed: boolean | undefined = undefined;
theme: string | undefined = undefined;
constructor(private store: RootStore) {
makeObservable(this, {
// observables
isNewUserPopup: observable.ref,
isSidebarCollapsed: observable.ref,
theme: observable.ref,
// action
toggleNewUserPopup: action,
toggleSidebar: action,
setTheme: action,
});
}
/**
* Toggle the sidebar collapsed state
* @description Toggle the new user popup modal
*/
toggleNewUserPopup = () => (this.isNewUserPopup = !this.isNewUserPopup);
/**
* @description Toggle the sidebar collapsed state
* @param isCollapsed
*/
toggleSidebar = (isCollapsed: boolean) => {
@ -39,7 +49,7 @@ export class ThemeStore implements IThemeStore {
};
/**
* Sets the user theme and applies it to the platform
* @description Sets the user theme and applies it to the platform
* @param currentTheme
*/
setTheme = async (currentTheme: TTheme) => {

View File

@ -3,11 +3,10 @@ import { IUser } from "@plane/types";
// helpers
import { EUserStatus, TUserStatus } from "@/helpers";
// services
import { UserService } from "services/user.service";
import { UserService } from "@/services/user.service";
// root store
import { RootStore } from "@/store/root-store";
import { AuthService } from "@/services";
import { API_BASE_URL } from "@/helpers/common.helper";
export interface IUserStore {
// observables
@ -79,7 +78,6 @@ export class UserStore implements IUserStore {
};
signOut = async () => {
await this.authService.signOut(API_BASE_URL);
this.rootStore.resetOnSignOut();
};
}

View File

@ -32,7 +32,6 @@ from plane.api.serializers import (
LabelSerializer,
)
from plane.app.permissions import (
WorkspaceEntityPermission,
ProjectEntityPermission,
ProjectLitePermission,
ProjectMemberPermission,

View File

@ -38,11 +38,13 @@ class SessionMiddleware(MiddlewareMixin):
return response
# First check if we need to delete this cookie.
# The session should be deleted only if the session is entirely empty.
is_admin_path = "instances" in request.path
cookie_name = (
settings.ADMIN_SESSION_COOKIE_NAME
if "instances" in request.path
if is_admin_path
else settings.SESSION_COOKIE_NAME
)
if cookie_name in request.COOKIES and empty:
response.delete_cookie(
cookie_name,
@ -59,11 +61,16 @@ class SessionMiddleware(MiddlewareMixin):
max_age = None
expires = None
else:
max_age = request.session.get_expiry_age()
# Use different max_age based on whether it's an admin cookie
if is_admin_path:
max_age = settings.ADMIN_SESSION_COOKIE_AGE
else:
max_age = request.session.get_expiry_age()
expires_time = time.time() + max_age
expires = http_date(expires_time)
# Save the session data and refresh the client cookie.
# Skip session save for 5xx responses.
if response.status_code < 500:
try:
request.session.save()

View File

@ -44,7 +44,7 @@ class MagicCodeProvider(CredentialAdapter):
)
)
if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD):
if not (EMAIL_HOST):
raise ImproperlyConfigured(
"SMTP is not configured. Please contact the support team."
)

View File

@ -96,7 +96,7 @@ class ForgotPasswordEndpoint(APIView):
)
)
if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD):
if not (EMAIL_HOST):
return Response(
{
"error_code": "SMTP_NOT_CONFIGURED",

View File

@ -150,8 +150,6 @@ class InstanceEndpoint(BaseAPIView):
# is smtp configured
data["is_smtp_configured"] = (
bool(EMAIL_HOST)
and bool(EMAIL_HOST_USER)
and bool(EMAIL_HOST_PASSWORD)
)
instance_data = serializer.data
instance_data["workspaces_exist"] = Workspace.objects.count() > 1

View File

@ -333,6 +333,7 @@ SESSION_SAVE_EVERY_REQUEST = True
# Admin Cookie
ADMIN_SESSION_COOKIE_NAME = "plane-admin-session-id"
ADMIN_SESSION_COOKIE_AGE = 3600
# CSRF cookies
CSRF_COOKIE_SECURE = secure_origins

View File

@ -1,37 +1,63 @@
# base requirements
# django
Django==4.2.11
psycopg==3.1.12
djangorestframework==3.14.0
redis==4.6.0
django-cors-headers==4.2.0
whitenoise==6.5.0
django-allauth==0.55.2
faker==18.11.2
django-filter==23.2
jsonmodels==2.6.0
djangorestframework-simplejwt==5.3.0
sentry-sdk==1.30.0
django-storages==1.14
django-crum==0.7.9
google-auth==2.22.0
google-api-python-client==2.97.0
django-redis==5.3.0
uvicorn==0.23.2
channels==4.0.0
openai==1.2.4
slack-sdk==3.21.3
celery==5.3.4
django_celery_beat==2.5.0
psycopg-binary==3.1.12
psycopg-c==3.1.12
scout-apm==2.26.1
openpyxl==3.1.2
python-json-logger==2.0.7
beautifulsoup4==4.12.2
# rest framework
djangorestframework==3.15.1
# postgres
psycopg==3.1.18
psycopg-binary==3.1.18
psycopg-c==3.1.18
dj-database-url==2.1.0
posthog==3.0.2
cryptography==42.0.4
lxml==4.9.3
boto3==1.28.40
# redis
redis==5.0.4
django-redis==5.4.0
# cors
django-cors-headers==4.3.1
# celery
celery==5.4.0
django_celery_beat==2.6.0
# file serve
whitenoise==6.6.0
# fake data
faker==25.0.0
# filters
django-filter==24.2
# json model
jsonmodels==2.7.0
# sentry
sentry-sdk==2.0.1
# storage
django-storages==1.14.2
# user management
django-crum==0.7.9
# web server
uvicorn==0.29.0
# sockets
channels==4.1.0
# ai
openai==1.25.0
# slack
slack-sdk==3.27.1
# apm
scout-apm==3.1.0
# xlsx generation
openpyxl==3.1.2
# logging
python-json-logger==2.0.7
# html parser
beautifulsoup4==4.12.3
# analytics
posthog==3.5.0
# crypto
cryptography==42.0.5
# html validator
lxml==5.2.1
# s3
boto3==1.34.96
# password validator
zxcvbn==4.4.28
# timezone
pytz==2024.1
# jwt
jwt==1.3.1

View File

@ -1,3 +1,5 @@
-r base.txt
django-debug-toolbar==4.1.0
# debug toolbar
django-debug-toolbar==4.3.0
# formatter
ruff==0.4.2

View File

@ -1,3 +1,3 @@
-r base.txt
# server
gunicorn==22.0.0

View File

@ -1,4 +1,4 @@
-r base.txt
# test checker
pytest==7.1.2
coverage==6.5.0

View File

@ -3,11 +3,11 @@ import { Controller, useForm } from "react-hook-form";
// icons
import { XCircle, CircleAlert } from "lucide-react";
// ui
import { Button, Input } from "@plane/ui";
import { Button, Input, Spinner } from "@plane/ui";
// helpers
import { checkEmailValidity } from "@/helpers/string.helper";
// types
import { IEmailCheckData } from "types/auth";
import { IEmailCheckData } from "@/types/auth";
type Props = {
onSubmit: (data: IEmailCheckData) => Promise<void>;
@ -40,7 +40,7 @@ export const EmailForm: React.FC<Props> = (props) => {
};
return (
<form onSubmit={handleSubmit(handleFormSubmit)} className="mx-auto mt-8 space-y-4 w-5/6 sm:w-96">
<form onSubmit={handleSubmit(handleFormSubmit)} className="mt-8 space-y-4">
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email
@ -84,8 +84,8 @@ export const EmailForm: React.FC<Props> = (props) => {
)}
/>
</div>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={!isValid} loading={isSubmitting}>
Continue
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={!isValid || isSubmitting}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
</form>
);

View File

@ -4,7 +4,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { Eye, EyeOff, XCircle } from "lucide-react";
// ui
import { Button, Input } from "@plane/ui";
import { Button, Input, Spinner } from "@plane/ui";
import { EAuthModes, EAuthSteps, ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/accounts";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
@ -40,6 +40,7 @@ export const PasswordForm: React.FC<Props> = (props) => {
const [showPassword, setShowPassword] = useState(false);
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// hooks
const {
instanceStore: { instance },
@ -82,6 +83,7 @@ export const PasswordForm: React.FC<Props> = (props) => {
const isButtonDisabled = useMemo(
() =>
!isSubmitting &&
!!passwordFormData.password &&
(mode === EAuthModes.SIGN_UP
? getPasswordStrength(passwordFormData.password) >= 3 &&
@ -89,14 +91,16 @@ export const PasswordForm: React.FC<Props> = (props) => {
: true)
? false
: true,
[mode, passwordFormData]
[isSubmitting, mode, passwordFormData.confirm_password, passwordFormData.password]
);
return (
<form
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
className="mt-5 space-y-4"
method="POST"
action={`${API_BASE_URL}/auth/spaces/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`}
onSubmit={() => setIsSubmitting(true)}
onError={() => setIsSubmitting(false)}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<input type="hidden" name="next_path" value={next_path} />
@ -153,7 +157,7 @@ export const PasswordForm: React.FC<Props> = (props) => {
</div>
{passwordSupport}
</div>
{mode === EAuthModes.SIGN_UP && getPasswordStrength(passwordFormData.password) >= 3 && (
{mode === EAuthModes.SIGN_UP && (
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
Confirm password
@ -188,7 +192,7 @@ export const PasswordForm: React.FC<Props> = (props) => {
{mode === EAuthModes.SIGN_IN ? (
<>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
Go to board
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Go to board"}
</Button>
{instance && isSmtpConfigured && (
<Button
@ -204,7 +208,7 @@ export const PasswordForm: React.FC<Props> = (props) => {
</>
) : (
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
Create account
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Create account"}
</Button>
)}
</div>

View File

@ -117,44 +117,42 @@ export const AuthRoot = observer(() => {
const isOAuthEnabled =
instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled);
return (
<>
<div className="mx-auto flex flex-col">
<div className="text-center space-y-1 py-4 mx-auto sm:w-96">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">{header}</h3>
<p className="font-medium text-onboarding-text-400">{subHeader}</p>
</div>
{authStep === EAuthSteps.EMAIL && <EmailForm onSubmit={handelEmailVerification} />}
{authMode && (
<>
{authStep === EAuthSteps.PASSWORD && (
<PasswordForm
email={email}
mode={authMode}
handleEmailClear={() => {
setEmail("");
setAuthMode(null);
setAuthStep(EAuthSteps.EMAIL);
}}
handleStepChange={(step) => setAuthStep(step)}
/>
)}
{authStep === EAuthSteps.UNIQUE_CODE && (
<UniqueCodeForm
email={email}
mode={authMode}
handleEmailClear={() => {
setEmail("");
setAuthMode(null);
setAuthStep(EAuthSteps.EMAIL);
}}
submitButtonText="Continue"
/>
)}
</>
)}
<div className="relative flex flex-col space-y-6">
<div className="space-y-1 text-center">
<h3 className="text-3xl font-bold text-onboarding-text-100">{header}</h3>
<p className="font-medium text-onboarding-text-400">{subHeader}</p>
</div>
{authStep === EAuthSteps.EMAIL && <EmailForm onSubmit={handelEmailVerification} />}
{authMode && (
<>
{authStep === EAuthSteps.PASSWORD && (
<PasswordForm
email={email}
mode={authMode}
handleEmailClear={() => {
setEmail("");
setAuthMode(null);
setAuthStep(EAuthSteps.EMAIL);
}}
handleStepChange={(step) => setAuthStep(step)}
/>
)}
{authStep === EAuthSteps.UNIQUE_CODE && (
<UniqueCodeForm
email={email}
mode={authMode}
handleEmailClear={() => {
setEmail("");
setAuthMode(null);
setAuthStep(EAuthSteps.EMAIL);
}}
submitButtonText="Continue"
/>
)}
</>
)}
{isOAuthEnabled && <OAuthOptions />}
<TermsAndConditions mode={authMode} />
</>
</div>
);
});

View File

@ -1,18 +1,18 @@
import React, { useEffect, useState } from "react";
// hooks
import useTimer from "hooks/use-timer";
import useToast from "hooks/use-toast";
import { useRouter } from "next/router";
// types
import { IEmailCheckData } from "types/auth";
// icons
import { CircleCheck, XCircle } from "lucide-react";
// ui
import { Button, Input } from "@plane/ui";
import { Button, Input, Spinner } from "@plane/ui";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// hooks
import useTimer from "@/hooks/use-timer";
import useToast from "@/hooks/use-toast";
// services
import { AuthService } from "@/services/authentication.service";
// types
import { IEmailCheckData } from "@/types/auth";
import { EAuthModes } from "./root";
type Props = {
@ -41,6 +41,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
const [uniqueCodeFormData, setUniqueCodeFormData] = useState<TUniqueCodeFormValues>({ ...defaultValues, email });
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isSubmitting, setIsSubmitting] = useState(false);
// router
const router = useRouter();
const { next_path } = router.query;
@ -99,12 +100,15 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
}, []);
const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0;
const isButtonDisabled = isRequestingNewCode || !uniqueCodeFormData.code || isSubmitting;
return (
<form
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
className="mt-5 space-y-4"
method="POST"
action={`${API_BASE_URL}/auth/spaces/${mode === EAuthModes.SIGN_IN ? "magic-sign-in" : "magic-sign-up"}/`}
onSubmit={() => setIsSubmitting(true)}
onError={() => setIsSubmitting(false)}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<input type="hidden" name="next_path" value={next_path} />
@ -170,15 +174,14 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
</button>
</div>
</div>
<Button
type="submit"
variant="primary"
className="w-full"
size="lg"
loading={isRequestingNewCode}
disabled={isRequestingNewCode || !uniqueCodeFormData.code}
>
{isRequestingNewCode ? "Sending code" : submitButtonText}
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
{isRequestingNewCode ? (
"Sending code"
) : isSubmitting ? (
<Spinner height="20px" width="20px" />
) : (
submitButtonText
)}
</Button>
</form>
);

View File

@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form";
// types
import { IUser } from "@plane/types";
// ui
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { UserImageUploadModal } from "@/components/accounts";
// hooks
@ -93,7 +93,7 @@ export const OnBoardingForm: React.FC<Props> = observer((props) => {
});
};
const isButtonDisabled = useMemo(() => (isValid ? false : true), [isValid]);
const isButtonDisabled = useMemo(() => (isValid && !isSubmitting ? false : true), [isSubmitting, isValid]);
return (
<form onSubmit={handleSubmit(onSubmit)} className="w-full mx-auto mt-2 space-y-4 sm:w-96">
@ -202,15 +202,8 @@ export const OnBoardingForm: React.FC<Props> = observer((props) => {
{errors.last_name && <span className="text-sm text-red-500">{errors.last_name.message}</span>}
</div>
</div>
<Button
variant="primary"
type="submit"
size="lg"
className="w-full"
disabled={isButtonDisabled}
loading={isSubmitting}
>
Continue
<Button variant="primary" type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
</form>
);

View File

@ -11,8 +11,8 @@ import { AuthRoot, UserLoggedIn } from "@/components/accounts";
import useAuthRedirection from "@/hooks/use-auth-redirection";
import { useMobxStore } from "@/lib/mobx/store-provider";
// images
import PlaneBackgroundPatternDark from "public/onboarding/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text-new.png";
export const AuthView = observer(() => {
@ -46,25 +46,23 @@ export const AuthView = observer(() => {
{currentUser ? (
<UserLoggedIn />
) : (
<div className="relative w-full h-screen overflow-hidden">
<div className="relative w-screen h-screen overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-screen min-h-screen object-cover"
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10">
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
<div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
</div>
<div className="mx-auto h-full">
<div className="h-full overflow-auto px-7 pt-4 sm:px-0">
<AuthRoot />
</div>
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
<AuthRoot />
</div>
</div>
</div>

View File

@ -37,6 +37,7 @@
"nprogress": "^0.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.38.0",
"swr": "^2.2.2",
"tailwind-merge": "^2.0.0",

View File

@ -17,8 +17,8 @@ import useTimer from "@/hooks/use-timer";
// services
import { AuthService } from "@/services/authentication.service";
// images
import PlaneBackgroundPatternDark from "public/onboarding/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
type TForgotPasswordFormValues = {
@ -79,9 +79,9 @@ const ForgotPasswordPage: NextPage = () => {
return (
<div className="relative h-screen w-full overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-screen min-h-screen object-cover"
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>
@ -159,5 +159,4 @@ const ForgotPasswordPage: NextPage = () => {
);
};
export default ForgotPasswordPage;

View File

@ -16,8 +16,8 @@ import { getPasswordStrength } from "@/helpers/password.helper";
// services
import { AuthService } from "@/services/authentication.service";
// images
import PlaneBackgroundPatternDark from "public/onboarding/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
type TResetPasswordFormValues = {
@ -79,7 +79,7 @@ const ResetPasswordPage: NextPage = () => {
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-screen min-h-screen object-cover"
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>

View File

@ -15,7 +15,7 @@ import ProfileSetup from "public/onboarding/profile-setup.svg";
const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
const OnBoardingPage = () => {
const OnBoardingPage = observer(() => {
// router
const router = useRouter();
const { next_path } = router.query;
@ -30,7 +30,7 @@ const OnBoardingPage = () => {
const user = userStore?.currentUser;
if (!user) {
router.push("/");
return;
return <></>;
}
// complete onboarding
@ -117,6 +117,6 @@ const OnBoardingPage = () => {
</div>
</div>
);
};
});
export default observer(OnBoardingPage);
export default OnBoardingPage;

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -3,6 +3,7 @@
"globalEnv": [
"NODE_ENV",
"NEXT_PUBLIC_API_BASE_URL",
"NEXT_PUBLIC_APP_URL",
"NEXT_PUBLIC_DEPLOY_URL",
"NEXT_PUBLIC_GOD_MODE_URL",
"NEXT_PUBLIC_SENTRY_DSN",

View File

@ -13,7 +13,7 @@ export const AuthBanner: FC<TAuthBanner> = (props) => {
if (!bannerData) return <></>;
return (
<div className="relative inline-flex items-center p-2 rounded-md gap-2 border border-custom-primary-100/50 bg-custom-primary-100/10">
<div className="relative flex items-center p-2 rounded-md gap-2 border border-custom-primary-100/50 bg-custom-primary-100/10">
<div className="w-4 h-4 flex-shrink-0 relative flex justify-center items-center">
<Info size={16} className="text-custom-primary-100" />
</div>

View File

@ -1,5 +1,7 @@
import { FC, useEffect, useState } from "react";
import { FC, ReactNode } from "react";
import useSWR from "swr";
import { IWorkspaceMemberInvitation } from "@plane/types";
import { Spinner } from "@plane/ui";
// components
import { WorkspaceLogo } from "@/components/workspace/logo";
// helpers
@ -13,7 +15,7 @@ type TAuthHeader = {
invitationEmail: string | undefined;
authMode: EAuthModes;
currentAuthStep: EAuthSteps;
handleLoader: (isLoading: boolean) => void;
children: ReactNode;
};
const Titles = {
@ -50,9 +52,16 @@ const Titles = {
const workSpaceService = new WorkspaceService();
export const AuthHeader: FC<TAuthHeader> = (props) => {
const { workspaceSlug, invitationId, invitationEmail, authMode, currentAuthStep, handleLoader } = props;
// state
const [invitation, setInvitation] = useState<IWorkspaceMemberInvitation | undefined>(undefined);
const { workspaceSlug, invitationId, invitationEmail, authMode, currentAuthStep, children } = props;
const { data: invitation, isLoading } = useSWR(
workspaceSlug && invitationId ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null,
async () => workspaceSlug && invitationId && workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId),
{
revalidateOnFocus: false,
shouldRetryOnError: false,
}
);
const getHeaderSubHeader = (
step: EAuthSteps,
@ -64,9 +73,10 @@ export const AuthHeader: FC<TAuthHeader> = (props) => {
const workspace = invitation.workspace;
return {
header: (
<>
Join <WorkspaceLogo logo={workspace?.logo} name={workspace?.name} classNames="w-8 h-9" /> {workspace.name}
</>
<div className="relative inline-flex items-center gap-2">
Join <WorkspaceLogo logo={workspace?.logo} name={workspace?.name} classNames="w-8 h-9 flex-shrink-0" />{" "}
{workspace.name}
</div>
),
subHeader: `${
mode == EAuthModes.SIGN_UP ? "Create an account" : "Sign in"
@ -77,23 +87,22 @@ export const AuthHeader: FC<TAuthHeader> = (props) => {
return Titles[mode][step];
};
useEffect(() => {
if (workspaceSlug && invitationId) {
handleLoader(true);
workSpaceService
.getWorkspaceInvitation(workspaceSlug, invitationId)
.then((res) => setInvitation(res))
.catch(() => setInvitation(undefined))
.finally(() => handleLoader(false));
} else setInvitation(undefined);
}, [workspaceSlug, invitationId, handleLoader]);
const { header, subHeader } = getHeaderSubHeader(currentAuthStep, authMode, invitation || undefined, invitationEmail);
const { header, subHeader } = getHeaderSubHeader(currentAuthStep, authMode, invitation, invitationEmail);
if (isLoading)
return (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
);
return (
<div className="space-y-1 text-center">
<h3 className="text-3xl font-bold text-onboarding-text-100">{header}</h3>
<p className="font-medium text-onboarding-text-400">{subHeader}</p>
</div>
<>
<div className="space-y-1 text-center">
<h3 className="text-3xl font-bold text-onboarding-text-100">{header}</h3>
<p className="font-medium text-onboarding-text-400">{subHeader}</p>
</div>
{children}
</>
);
};

View File

@ -5,7 +5,7 @@ import { CircleAlert, XCircle } from "lucide-react";
// types
import { IEmailCheckData } from "@plane/types";
// ui
import { Button, Input } from "@plane/ui";
import { Button, Input, Spinner } from "@plane/ui";
// helpers
import { checkEmailValidity } from "@/helpers/string.helper";
@ -35,8 +35,10 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
setIsSubmitting(false);
};
const isButtonDisabled = email.length === 0 || Boolean(emailError?.email) || isSubmitting;
return (
<form onSubmit={handleFormSubmit} className="mx-auto mt-8 space-y-4 w-5/6 sm:w-96">
<form onSubmit={handleFormSubmit} className="mt-8 space-y-4">
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email
@ -67,15 +69,8 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
</p>
)}
</div>
<Button
type="submit"
variant="primary"
className="w-full"
size="lg"
disabled={email.length === 0 || Boolean(emailError?.email)}
loading={isSubmitting}
>
Continue
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
</form>
);

View File

@ -4,7 +4,7 @@ import Link from "next/link";
// icons
import { Eye, EyeOff, XCircle } from "lucide-react";
// ui
import { Button, Input } from "@plane/ui";
import { Button, Input, Spinner } from "@plane/ui";
// components
import { ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/account";
// constants
@ -45,6 +45,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
const [showPassword, setShowPassword] = useState(false);
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// hooks
const { instance } = useInstance();
const { captureEvent } = useEventTracker();
@ -84,6 +85,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
const isButtonDisabled = useMemo(
() =>
!isSubmitting &&
!!passwordFormData.password &&
(mode === EAuthModes.SIGN_UP
? getPasswordStrength(passwordFormData.password) >= 3 &&
@ -91,14 +93,16 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
: true)
? false
: true,
[mode, passwordFormData]
[isSubmitting, mode, passwordFormData.confirm_password, passwordFormData.password]
);
return (
<form
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
className="mt-5 space-y-4"
method="POST"
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`}
onSubmit={() => setIsSubmitting(true)}
onError={() => setIsSubmitting(false)}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="space-y-1">
@ -189,7 +193,13 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
{mode === EAuthModes.SIGN_IN ? (
<>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
{isSmtpConfigured ? "Continue" : "Go to workspace"}
{isSubmitting ? (
<Spinner height="20px" width="20px" />
) : isSmtpConfigured ? (
"Continue"
) : (
"Go to workspace"
)}
</Button>
{instance && isSmtpConfigured && (
<Button
@ -205,11 +215,10 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
</>
) : (
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
Create account
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Create account"}
</Button>
)}
</div>
</form>
);
});

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { IEmailCheckData } from "@plane/types";
import { Spinner, TOAST_TYPE, setToast } from "@plane/ui";
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import {
AuthHeader,
@ -11,7 +11,7 @@ import {
AuthPasswordForm,
OAuthOptions,
TermsAndConditions,
UniqueCodeForm,
AuthUniqueCodeForm,
} from "@/components/account";
// helpers
import {
@ -36,7 +36,6 @@ export const SignInAuthRoot = observer(() => {
// states
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
const [isLoading, setIsLoading] = useState(false);
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
// hooks
const { instance } = useInstance();
@ -74,30 +73,21 @@ export const SignInAuthRoot = observer(() => {
const isOAuthEnabled =
instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled);
if (isLoading)
return (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
);
return (
<>
<div className="relative max-w-lg mx-auto flex flex-col space-y-6">
<AuthHeader
workspaceSlug={workspaceSlug?.toString() || undefined}
invitationId={invitation_id?.toString() || undefined}
invitationEmail={email || undefined}
authMode={EAuthModes.SIGN_IN}
currentAuthStep={authStep}
handleLoader={setIsLoading}
/>
<div className="relative flex flex-col space-y-6">
<AuthHeader
workspaceSlug={workspaceSlug?.toString() || undefined}
invitationId={invitation_id?.toString() || undefined}
invitationEmail={email || undefined}
authMode={EAuthModes.SIGN_IN}
currentAuthStep={authStep}
>
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
)}
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
{authStep === EAuthSteps.UNIQUE_CODE && (
<UniqueCodeForm
<AuthUniqueCodeForm
email={email}
handleEmailClear={() => {
setEmail("");
@ -120,7 +110,7 @@ export const SignInAuthRoot = observer(() => {
)}
{isOAuthEnabled && <OAuthOptions />}
<TermsAndConditions isSignUp={false} />
</div>
</>
</AuthHeader>
</div>
);
});

View File

@ -4,7 +4,7 @@ import { useRouter } from "next/router";
// types
import { IEmailCheckData } from "@plane/types";
// ui
import { Spinner, TOAST_TYPE, setToast } from "@plane/ui";
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import {
AuthHeader,
@ -39,7 +39,6 @@ export const SignUpAuthRoot: FC = observer(() => {
// states
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
const [isLoading, setIsLoading] = useState(false);
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
// hooks
const { instance } = useInstance();
@ -63,7 +62,7 @@ export const SignUpAuthRoot: FC = observer(() => {
await authService
.signUpEmailCheck(data)
.then(() => {
if (isSmtpConfigured) setAuthStep(EAuthSteps.PASSWORD);
if (isSmtpConfigured) setAuthStep(EAuthSteps.UNIQUE_CODE);
else setAuthStep(EAuthSteps.PASSWORD);
})
.catch((error) => {
@ -83,51 +82,44 @@ export const SignUpAuthRoot: FC = observer(() => {
const isOAuthEnabled =
instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled);
if (isLoading)
return (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
);
return (
<div className="relative max-w-lg px-5 mx-auto flex flex-col space-y-6">
<div className="relative flex flex-col space-y-6">
<AuthHeader
workspaceSlug={workspaceSlug?.toString() || undefined}
invitationId={invitation_id?.toString() || undefined}
invitationEmail={email || undefined}
authMode={EAuthModes.SIGN_UP}
currentAuthStep={authStep}
handleLoader={setIsLoading}
/>
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
)}
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
{authStep === EAuthSteps.UNIQUE_CODE && (
<AuthUniqueCodeForm
email={email}
handleEmailClear={() => {
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
}}
submitButtonText="Continue"
mode={authMode}
/>
)}
{authStep === EAuthSteps.PASSWORD && (
<AuthPasswordForm
email={email}
handleEmailClear={() => {
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
}}
handleStepChange={(step) => setAuthStep(step)}
mode={authMode}
/>
)}
{isOAuthEnabled && <OAuthOptions />}
<TermsAndConditions isSignUp={authMode === EAuthModes.SIGN_UP} />
>
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
)}
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
{authStep === EAuthSteps.UNIQUE_CODE && (
<AuthUniqueCodeForm
email={email}
handleEmailClear={() => {
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
}}
submitButtonText="Continue"
mode={authMode}
/>
)}
{authStep === EAuthSteps.PASSWORD && (
<AuthPasswordForm
email={email}
handleEmailClear={() => {
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
}}
handleStepChange={(step) => setAuthStep(step)}
mode={authMode}
/>
)}
{isOAuthEnabled && <OAuthOptions />}
<TermsAndConditions isSignUp={authMode === EAuthModes.SIGN_UP} />
</AuthHeader>
</div>
);
});

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { CircleCheck, XCircle } from "lucide-react";
import { IEmailCheckData } from "@plane/types";
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { EAuthModes } from "@/helpers/authentication.helper";
import { API_BASE_URL } from "@/helpers/common.helper";
@ -36,6 +36,7 @@ export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
const [uniqueCodeFormData, setUniqueCodeFormData] = useState<TUniqueCodeFormValues>({ ...defaultValues, email });
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isSubmitting, setIsSubmitting] = useState(false);
// store hooks
// const { captureEvent } = useEventTracker();
// timer
@ -88,12 +89,15 @@ export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
}, []);
const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0;
const isButtonDisabled = isRequestingNewCode || !uniqueCodeFormData.code || isSubmitting;
return (
<form
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
className="mt-5 space-y-4"
method="POST"
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "magic-sign-in" : "magic-sign-up"}/`}
onSubmit={() => setIsSubmitting(true)}
onError={() => setIsSubmitting(false)}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="space-y-1">
@ -159,15 +163,14 @@ export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
</button>
</div>
</div>
<Button
type="submit"
variant="primary"
className="w-full"
size="lg"
loading={isRequestingNewCode}
disabled={isRequestingNewCode || !uniqueCodeFormData.code}
>
{isRequestingNewCode ? "Sending code" : submitButtonText}
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
{isRequestingNewCode ? (
"Sending code"
) : isSubmitting ? (
<Spinner height="20px" width="20px" />
) : (
submitButtonText
)}
</Button>
</form>
);

View File

@ -1,62 +0,0 @@
// react
import { useEffect, useState, FC } from "react";
// next
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/router";
import { useTheme } from "next-themes";
// images
import githubLightModeImage from "/public/logos/github-black.png";
import githubDarkModeImage from "/public/logos/github-dark.svg";
type Props = {
handleSignIn: React.Dispatch<string>;
clientId: string;
type: "sign_in" | "sign_up";
};
export const GitHubSignInButton: FC<Props> = (props) => {
const { handleSignIn, clientId, type } = props;
// states
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
const [gitCode, setGitCode] = useState<null | string>(null);
// router
const {
query: { code },
} = useRouter();
// theme
const { resolvedTheme } = useTheme();
useEffect(() => {
if (code && !gitCode) {
setGitCode(code.toString());
handleSignIn(code.toString());
}
}, [code, gitCode, handleSignIn]);
useEffect(() => {
const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
setLoginCallBackURL(`${origin}/` as any);
}, []);
return (
<div className="w-full">
<Link
href={`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
>
<button
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-onboarding-background-300 ${
resolvedTheme === "dark" ? "border-[#43484F] bg-[#2F3135]" : "border-[#D9E4FF]"
}`}
>
<Image
src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
height={18}
width={18}
alt="GitHub Logo"
/>
<span className="text-onboarding-text-200">{type === "sign_in" ? "Sign-in" : "Sign-up"} with GitHub</span>
</button>
</Link>
</div>
);
};

View File

@ -1,60 +0,0 @@
import { FC, useEffect, useRef, useCallback, useState } from "react";
import Script from "next/script";
type Props = {
handleSignIn: React.Dispatch<any>;
clientId: string;
type: "sign_in" | "sign_up";
};
export const GoogleSignInButton: FC<Props> = (props) => {
const { handleSignIn, clientId, type } = props;
// refs
const googleSignInButton = useRef<HTMLDivElement>(null);
// states
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
const loadScript = useCallback(() => {
if (!googleSignInButton.current || gsiScriptLoaded) return;
window?.google?.accounts.id.initialize({
client_id: clientId,
callback: handleSignIn,
});
try {
window?.google?.accounts.id.renderButton(
googleSignInButton.current,
{
type: "standard",
theme: "outline",
size: "large",
logo_alignment: "center",
text: type === "sign_in" ? "signin_with" : "signup_with",
} as GsiButtonConfiguration // customization attributes
);
} catch (err) {
console.log(err);
}
window?.google?.accounts.id.prompt(); // also display the One Tap dialog
setGsiScriptLoaded(true);
}, [handleSignIn, gsiScriptLoaded, clientId, type]);
useEffect(() => {
if (window?.google?.accounts?.id) {
loadScript();
}
return () => {
window?.google?.accounts.id.cancel();
};
}, [loadScript]);
return (
<>
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
<div className="!w-full overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
</>
);
};

View File

@ -1,5 +1,3 @@
export * from "./github-sign-in";
export * from "./google-sign-in";
export * from "./oauth-options";
export * from "./google-button";
export * from "./github-button";

View File

@ -10,12 +10,12 @@ export const OAuthOptions: React.FC = observer(() => {
return (
<>
<div className="mx-auto mt-4 flex items-center sm:w-96">
<div className="mt-4 flex items-center">
<hr className="w-full border-onboarding-border-100" />
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">or</p>
<hr className="w-full border-onboarding-border-100" />
</div>
<div className={`mx-auto mt-7 grid gap-4 overflow-hidden sm:w-96`}>
<div className={`mt-7 grid gap-4 overflow-hidden`}>
{instance?.config?.is_google_enabled && (
<div className="flex h-[42px] items-center !overflow-hidden">
<GoogleOAuthButton text="SignIn with Google" />

View File

@ -26,7 +26,7 @@ const inputRules = {
export const CustomThemeSelector: React.FC = observer(() => {
const {
profile: { data: userProfile },
userProfile: { data: userProfile },
} = useUser();
const userTheme: any = userProfile?.theme;

View File

@ -18,7 +18,6 @@ import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys";
import { EXPORTERS_LIST } from "@/constants/workspace";
// hooks
import { useUser } from "@/hooks/store";
import useUserAuth from "@/hooks/use-user-auth";
// services
import { IntegrationService } from "@/services/integrations";
@ -33,9 +32,7 @@ const IntegrationGuide = observer(() => {
const router = useRouter();
const { workspaceSlug, provider } = router.query;
// store hooks
const { data: currentUser, isLoading: currentUserLoader, profile } = useUser();
// custom hooks
const {} = useUserAuth({ user: currentUser || null, userProfile: profile?.data, isLoading: currentUserLoader });
const { data: currentUser } = useUser();
const { data: exporterServices } = useSWR(
workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null,

View File

@ -20,7 +20,7 @@ export const FilterMember: FC<Props> = observer((props: Props) => {
// hooks
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const { data: currentUser } = useUser();
// states
const [itemsToRender, setItemsToRender] = useState(5);
const [previewEnabled, setPreviewEnabled] = useState(true);

View File

@ -1,34 +1,31 @@
import { FC } from "react";
import Image from "next/image";
import { useTheme } from "next-themes";
import { Button } from "@plane/ui";
// images
import PlaneBlackLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.svg";
import PlaneWhiteLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.svg";
import PlaneTakeOffImage from "@/public/plane-takeoff.png";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
export const InstanceNotReady: FC = () => {
const { resolvedTheme } = useTheme();
const planeLogo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo;
const planeGodModeUrl = `${process.env.NEXT_PUBLIC_GOD_MODE_URL}/god-mode/setup/?auth_enabled=0`;
return (
<div className="relative h-screen max-h-max w-full overflow-hidden overflow-y-auto flex flex-col">
<div className="flex-shrink-0 h-[120px]">
<div className="flex-shrink-0 h-[100px]">
<div className="relative h-full container mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
<div className="flex items-center gap-x-2">
<Image src={planeLogo} className="h-[24px] w-full" alt="Plane logo" />
<div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
</div>
</div>
<div className="w-full flex-grow">
<div className="w-full flex-grow px-5 lg:px-0 mb-[100px]">
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
<Image src={PlaneTakeOffImage} alt="Plane Logo" />
<p className="font-medium text-base text-custom-text-400">
<p className="font-medium text-base text-onboarding-text-400">
Get started by setting up your instance and workspace
</p>
</div>

View File

@ -20,7 +20,6 @@ import { IMPORTER_SERVICES_LIST } from "@/constants/fetch-keys";
import { IMPORTERS_LIST } from "@/constants/workspace";
// hooks
import { useUser } from "@/hooks/store";
import useUserAuth from "@/hooks/use-user-auth";
// services
import { IntegrationService } from "@/services/integrations";
@ -36,9 +35,7 @@ const IntegrationGuide = observer(() => {
const router = useRouter();
const { workspaceSlug, provider } = router.query;
// store hooks
const { data: currentUser, isLoading: currentUserLoader, profile } = useUser();
// custom hooks
const {} = useUserAuth({ user: currentUser || null, userProfile: profile?.data, isLoading: currentUserLoader });
const { data: currentUser } = useUser();
const { data: importerServices } = useSWR(
workspaceSlug ? IMPORTER_SERVICES_LIST(workspaceSlug as string) : null,

View File

@ -22,7 +22,7 @@ export const FilterAssignees: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const { data: currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;

View File

@ -22,7 +22,7 @@ export const FilterCreatedBy: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const { data: currentUser } = useUser();
const sortedOptions = useMemo(() => {
const filteredOptions = (memberIds || []).filter((memberId) =>

View File

@ -22,7 +22,7 @@ export const FilterMentions: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const { data: currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;

View File

@ -22,7 +22,7 @@ export const FilterLead: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const { data: currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;

View File

@ -22,7 +22,7 @@ export const FilterMembers: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const { data: currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;

View File

@ -3,7 +3,7 @@ import { Controller, useForm } from "react-hook-form";
// types
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
// ui
import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { Button, CustomSelect, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// constants
import { WORKSPACE_CREATED } from "@/constants/event-tracker";
import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "@/constants/workspace";
@ -116,6 +116,8 @@ export const CreateWorkspace: React.FC<Props> = (props) => {
});
};
const isButtonDisabled = !isValid || invalidSlug || isSubmitting;
return (
<div className="space-y-4">
{!!invitedWorkspaces && (
@ -256,8 +258,8 @@ export const CreateWorkspace: React.FC<Props> = (props) => {
)}
</div>
</div>
<Button variant="primary" type="submit" size="lg" className="w-full" disabled={!isValid || invalidSlug}>
{isSubmitting ? "Creating..." : "Continue"}
<Button variant="primary" type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
</form>
</div>

View File

@ -3,7 +3,7 @@ import useSWR from "swr";;
// types
import { IWorkspaceMemberInvitation } from "@plane/types";
// ui
import { Button, Checkbox } from "@plane/ui";
import { Button, Checkbox, Spinner } from "@plane/ui";
// constants
import { MEMBER_ACCEPTED } from "@/constants/event-tracker";
import { USER_WORKSPACE_INVITATIONS } from "@/constants/fetch-keys";
@ -127,7 +127,7 @@ export const Invitations: React.FC<Props> = (props) => {
onClick={submitInvitations}
disabled={isJoiningWorkspaces || !invitationsRespond.length}
>
Continue to workspace
{isJoiningWorkspaces ? <Spinner height="20px" width="20px" /> : "Continue to workspace"}
</Button>
<div className="mx-auto mt-4 flex items-center sm:w-96">
<hr className="w-full border-onboarding-border-100" />

View File

@ -18,7 +18,7 @@ import { Listbox, Transition } from "@headlessui/react";
// types
import { IUser, IWorkspace } from "@plane/types";
// ui
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// constants
import { MEMBER_INVITED } from "@/constants/event-tracker";
import { EUserWorkspaceRoles, ROLE, ROLE_DETAILS } from "@/constants/workspace";
@ -420,10 +420,9 @@ export const InviteMembers: React.FC<Props> = (props) => {
type="submit"
size="lg"
className="w-full"
disabled={isInvitationDisabled || !isValid}
loading={isSubmitting}
disabled={isInvitationDisabled || !isValid || isSubmitting}
>
Continue
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
<Button variant="link-neutral" size="lg" className="w-full" onClick={nextStep}>
Ill do it later

View File

@ -7,7 +7,7 @@ import { Eye, EyeOff } from "lucide-react";
// types
import { IUser, TUserProfile, TOnboardingSteps } from "@plane/types";
// ui
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { PasswordStrengthMeter } from "@/components/account";
import { UserImageUploadModal } from "@/components/core";
@ -248,6 +248,7 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
// Also handles the condition for optional password i.e if password field is optional it only checks for above validation if it's not empty.
const isButtonDisabled = useMemo(
() =>
!isSubmitting &&
isValid &&
(isPasswordAlreadySetup
? true
@ -258,7 +259,7 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
: true)
? false
: true,
[isValid, isPasswordAlreadySetup, isSignUpUsingMagicCode, password, confirmPassword]
[isSubmitting, isValid, isPasswordAlreadySetup, isSignUpUsingMagicCode, password, confirmPassword]
);
const isCurrentStepUserPersonalization = profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION;
@ -541,15 +542,8 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
</div>
</>
)}
<Button
variant="primary"
type="submit"
size="lg"
className="w-full"
disabled={isButtonDisabled}
loading={isSubmitting}
>
Continue
<Button variant="primary" type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
</form>
</div>

View File

@ -1,2 +1 @@
export * from "./signup";
export * from "./workspace-dashboard";

View File

@ -1,61 +0,0 @@
import React from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
// ui
import { useTheme } from "next-themes";
// components
import { SignUpAuthRoot } from "@/components/account";
import { PageHead } from "@/components/core";
// constants
import { NAVIGATE_TO_SIGNIN } from "@/constants/event-tracker";
// hooks
import { useEventTracker } from "@/hooks/store";
// assets
import PlaneBackgroundPatternDark from "public/onboarding/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
export const SignUpView = observer(() => {
// store hooks
const { captureEvent } = useEventTracker();
// hooks
const { resolvedTheme } = useTheme();
// login redirection hook
return (
<div className="relative">
<PageHead title="Sign Up" />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-screen min-h-screen object-cover"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10">
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
<div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
<div className="text-center text-sm font-medium text-onboarding-text-300">
Already have an account?{" "}
<Link
href="/accounts/sign-in"
onClick={() => captureEvent(NAVIGATE_TO_SIGNIN, {})}
className="font-semibold text-custom-primary-100 hover:underline"
>
Sign In
</Link>
</div>
</div>
<div className="mx-auto h-full">
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
<SignUpAuthRoot />
</div>
</div>
</div>
</div>
);
});

View File

@ -22,7 +22,7 @@ export const FilterCreatedBy: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const { data: currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;

View File

@ -24,7 +24,6 @@ export const PreferencesMobileHeader = () => {
<Link
key={index}
href={link.href}
onClick={() => console.log(router.asPath)}
className={cn(
"flex justify-around py-2 w-full",
router.asPath.includes(link.label.toLowerCase()) ? "border-b-2 border-custom-primary-100" : ""

View File

@ -22,7 +22,7 @@ export const FilterLead: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const { data: currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;

View File

@ -22,7 +22,7 @@ export const FilterMembers: React.FC<Props> = observer((props: Props) => {
const [previewEnabled, setPreviewEnabled] = useState(true);
// store hooks
const { getUserDetails } = useMember();
const { currentUser } = useUser();
const { data: currentUser } = useUser();
const appliedFiltersCount = appliedFilters?.length ?? 0;

View File

@ -15,7 +15,7 @@ import { useUser } from "@/hooks/store";
export const WorkspaceActiveCyclesUpgrade = observer(() => {
// store hooks
const {
profile: { data: userProfile },
userProfile: { data: userProfile },
} = useUser();
const isDarkMode = userProfile?.theme.theme === "dark";

View File

@ -27,8 +27,7 @@ export * from "./use-issue-detail";
// project inbox
export * from "./use-project-inbox";
export * from "./use-inbox-issues";
export * from "./use-user-profile";
export * from "./use-user";
export * from "./user";
export * from "./use-instance";
export * from "./use-app-theme";
export * from "./use-command-palette";

View File

@ -1,11 +0,0 @@
import { useContext } from "react";
// mobx store
import { StoreContext } from "@/lib/store-context";
// types
import { IUserSettingsStore } from "@/store/user/user-setting.store";
export const useCurrentUserSettings = (): IUserSettingsStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useCurrentUserSettings must be used within StoreProvider");
return context.user.currentUserSettings;
};

View File

@ -1,11 +0,0 @@
import { useContext } from "react";
// mobx store
import { StoreContext } from "@/lib/store-context";
// types
import { IProfileStore } from "store/user/profile.store";
export const useUserProfile = (): IProfileStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useUser must be used within StoreProvider");
return context.user.profile;
};

View File

@ -0,0 +1,3 @@
export * from "./user-user";
export * from "./user-user-profile";
export * from "./user-user-settings";

View File

@ -0,0 +1,11 @@
import { useContext } from "react";
// mobx store
import { StoreContext } from "@/lib/store-context";
// types
import { IUserProfileStore } from "@/store/user/profile.store";
export const useUserProfile = (): IUserProfileStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
return context.user.userProfile;
};

View File

@ -0,0 +1,11 @@
import { useContext } from "react";
// mobx store
import { StoreContext } from "@/lib/store-context";
// types
import { IUserSettingsStore } from "@/store/user/settings.store";
export const useUserSettings = (): IUserSettingsStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useUserSettings must be used within StoreProvider");
return context.user.userSettings;
};

View File

@ -1,108 +0,0 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router";
// types
import { IUserSettings, IWorkspace, TUserProfile } from "@plane/types";
import { useUserProfile } from "@/hooks/store";
// hooks
import { useWorkspace } from "@/hooks/store";
import { useCurrentUserSettings } from "./store/use-current-user-settings";
type TUseAuthRedirectionProps = {
error: any | null;
isRedirecting: boolean;
handleRedirection: () => Promise<void>;
};
const useAuthRedirection = (): TUseAuthRedirectionProps => {
// states
const [isRedirecting, setIsRedirecting] = useState(false);
const [error, setError] = useState<any | null>(null);
// router
const router = useRouter();
const { next_path } = router.query;
// mobx store
const { fetchUserProfile } = useUserProfile();
const { fetchCurrentUserSettings } = useCurrentUserSettings();
const { fetchWorkspaces } = useWorkspace();
const isValidURL = (url: string): boolean => {
const disallowedSchemes = /^(https?|ftp):\/\//i;
return !disallowedSchemes.test(url);
};
const getAuthRedirectionUrl = useCallback(
async (profile: TUserProfile | undefined) => {
try {
if (!profile) return;
// if the user is not onboarded, redirect them to the onboarding page
if (!profile.is_onboarded) {
return "/onboarding";
}
// if next_path is provided, redirect the user to that url
if (next_path) {
if (isValidURL(next_path.toString())) {
return next_path.toString();
} else {
return "/";
}
}
// Fetch the current user settings
const userSettings: IUserSettings | undefined = await fetchCurrentUserSettings();
const workspacesList: IWorkspace[] = await fetchWorkspaces();
// Extract workspace details
const workspaceSlug =
userSettings?.workspace?.last_workspace_slug || userSettings?.workspace?.fallback_workspace_slug;
// Redirect based on workspace details or to profile if not available
if (
workspaceSlug &&
workspacesList &&
workspacesList.filter((workspace) => workspace.slug === workspaceSlug).length > 0
)
return `/${workspaceSlug}`;
else return "/profile";
} catch (error) {
setIsRedirecting(false);
console.error("Error in handleSignInRedirection:", error);
setError(error);
}
},
[fetchCurrentUserSettings, fetchWorkspaces, router, next_path]
);
const updateUserInfo = useCallback(async () => {
setIsRedirecting(true);
await fetchUserProfile()
.then(async (profile) => {
await getAuthRedirectionUrl(profile)
.then((url: string | undefined) => {
if (url) {
router.push(url);
return;
}
setIsRedirecting(false);
})
.catch((err) => {
setIsRedirecting(false);
setError(err);
});
})
.catch((err) => {
setError(err);
setIsRedirecting(false);
});
}, [fetchUserProfile, getAuthRedirectionUrl]);
return {
error,
isRedirecting,
handleRedirection: updateUserInfo,
};
};
export default useAuthRedirection;

View File

@ -1,11 +1,9 @@
import { useCallback } from "react";
import useSWR from "swr";
// hooks
import useUserAuth from "@/hooks/use-user-auth";
import { IUser, TUserProfile } from "@plane/types";
// services
import { NotificationService } from "@/services/notification.service";
// types
import { IUser, TUserProfile } from "@plane/types";
const userNotificationServices = new NotificationService();
@ -16,8 +14,6 @@ const useUserIssueNotificationSubscription = (
projectId?: string | string[] | null,
issueId?: string | string[] | null
) => {
const {} = useUserAuth({ user: user, userProfile: profile, isLoading: false });
const { data, error, mutate } = useSWR(
workspaceSlug && projectId && issueId ? `SUBSCRIPTION_STATUE_${workspaceSlug}_${projectId}_${issueId}` : null,
workspaceSlug && projectId && issueId

View File

@ -1,114 +0,0 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
// services
import { WorkspaceService } from "@/services/workspace.service";
// types
import { IUser, TUserProfile } from "@plane/types";
const workspaceService = new WorkspaceService();
type Props = {
routeAuth?: "sign-in" | "onboarding" | "admin" | null;
user: IUser | null;
userProfile: TUserProfile | undefined;
isLoading: boolean;
};
const useUserAuth = (props: Props) => {
const { routeAuth, user, userProfile, isLoading } = props;
// states
const [isRouteAccess, setIsRouteAccess] = useState(true);
// router
const router = useRouter();
const { next_path } = router.query;
const isValidURL = (url: string): boolean => {
const disallowedSchemes = /^(https?|ftp):\/\//i;
return !disallowedSchemes.test(url);
};
useEffect(() => {
const handleWorkSpaceRedirection = async () => {
workspaceService.userWorkspaces().then(async (userWorkspaces) => {
if (!user || !userProfile) return;
const firstWorkspace = Object.values(userWorkspaces ?? {})?.[0];
const lastActiveWorkspace = userWorkspaces.find((workspace) => workspace.id === user?.last_workspace_id);
if (lastActiveWorkspace) {
router.push(`/${lastActiveWorkspace.slug}`);
return;
} else if (firstWorkspace) {
router.push(`/${firstWorkspace.slug}`);
return;
} else {
router.push(`/profile`);
return;
}
});
};
const handleUserRouteAuthentication = async () => {
if (user && user.is_active && userProfile) {
if (routeAuth === "sign-in") {
if (userProfile.is_onboarded) handleWorkSpaceRedirection();
else {
router.push("/onboarding");
return;
}
} else if (routeAuth === "onboarding") {
if (userProfile.is_onboarded) handleWorkSpaceRedirection();
else {
setIsRouteAccess(() => false);
return;
}
} else {
if (!userProfile.is_onboarded) {
router.push("/onboarding");
return;
} else {
setIsRouteAccess(() => false);
return;
}
}
} else {
// user is not active and we can redirect to no access page
router.push("/no-access");
// remove token
return;
}
};
if (routeAuth === null) {
setIsRouteAccess(() => false);
return;
} else {
if (!isLoading) {
setIsRouteAccess(() => true);
if (user) {
if (next_path) {
if (isValidURL(next_path.toString())) {
router.push(next_path.toString());
return;
} else {
router.push("/");
return;
}
} else handleUserRouteAuthentication();
} else {
if (routeAuth === "sign-in") {
setIsRouteAccess(() => false);
return;
} else {
router.push("/");
return;
}
}
}
}
}, [user, isLoading, routeAuth, router, next_path]);
return {
isLoading: isRouteAccess,
};
};
export default useUserAuth;

View File

@ -1,31 +0,0 @@
import { FC, ReactNode } from "react";
import { observer } from "mobx-react";
// hooks
import { useAppRouter } from "@/hooks/store";
// components
export interface IAdminAuthWrapper {
children: ReactNode;
}
export const AdminAuthWrapper: FC<IAdminAuthWrapper> = observer(({ children }) => {
// store hooks
const { workspaceSlug } = useAppRouter();
// FIXME:
// const { isUserInstanceAdmin, currentUserSettings } = useUser();
// redirect url
const redirectWorkspaceSlug =
workspaceSlug ||
// currentUserSettings?.workspace?.last_workspace_slug ||
// currentUserSettings?.workspace?.fallback_workspace_slug ||
"";
console.log("redirectWorkspaceSlug", redirectWorkspaceSlug);
// if user does not have admin access to the instance
// if (isUserInstanceAdmin !== undefined && isUserInstanceAdmin === false) {
// return <InstanceAdminRestriction redirectWorkspaceSlug={redirectWorkspaceSlug} />;
// }
return <>{children}</>;
});

View File

@ -1,4 +1,2 @@
export * from "./user-wrapper";
export * from "./workspace-wrapper";
export * from "./project-wrapper";
export * from "./admin-wrapper";

View File

@ -1,59 +0,0 @@
import { FC, ReactNode } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import useSWR from "swr";
// import useSWRImmutable from "swr/immutable";
// ui
import { Spinner } from "@plane/ui";
// hooks
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
import { useUser, useUserProfile, useWorkspace } from "@/hooks/store";
import { useCurrentUserSettings } from "@/hooks/store/use-current-user-settings";
export interface IUserAuthWrapper {
children: ReactNode;
}
export const UserAuthWrapper: FC<IUserAuthWrapper> = observer((props) => {
const { children } = props;
// store hooks
const { fetchCurrentUser, data: currentUser, error: currentUserError } = useUser();
const { fetchUserProfile } = useUserProfile();
const { fetchCurrentUserSettings } = useCurrentUserSettings();
const { fetchWorkspaces } = useWorkspace();
// router
const router = useRouter();
// fetching user information
const { error } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
shouldRetryOnError: false,
});
useSWR("CURRENT_USER_PROFILE_DETAILS", () => fetchUserProfile(), {
shouldRetryOnError: false,
});
//fetching user settings
const { isLoading: userSettingsLoader } = useSWR("CURRENT_USER_SETTINGS", () => fetchCurrentUserSettings(), {
shouldRetryOnError: false,
});
// fetching all workspaces
const { isLoading: workspaceLoader } = useSWR(USER_WORKSPACES_LIST, () => fetchWorkspaces(), {
shouldRetryOnError: false,
});
if ((!currentUser && !currentUserError) || workspaceLoader) {
return (
<div className="grid h-screen place-items-center bg-custom-background-100 p-4">
<div className="flex flex-col items-center gap-3 text-center">
<Spinner />
</div>
</div>
);
}
if (error) {
const redirectTo = router.asPath;
router.push(`/?next_path=${redirectTo}`);
return null;
}
return <>{children}</>;
});

View File

@ -1,8 +1,9 @@
import { FC, ReactNode } from "react";
// layout
import { CommandPalette } from "@/components/command-palette";
import { UserAuthWrapper } from "@/layouts/auth-layout";
import { ProfileLayoutSidebar } from "@/layouts/settings-layout";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers";
// components
interface IProfileSettingsLayout {
@ -16,7 +17,7 @@ export const ProfileSettingsLayout: FC<IProfileSettingsLayout> = (props) => {
return (
<>
<CommandPalette />
<UserAuthWrapper>
<AuthenticationWrapper>
<div className="relative flex h-screen w-full overflow-hidden">
<ProfileLayoutSidebar />
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
@ -24,7 +25,7 @@ export const ProfileSettingsLayout: FC<IProfileSettingsLayout> = (props) => {
<div className="h-full w-full overflow-hidden">{children}</div>
</main>
</div>
</UserAuthWrapper>
</AuthenticationWrapper>
</>
);
};

View File

@ -6,7 +6,7 @@ import { Spinner } from "@plane/ui";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useUser, useWorkspace } from "@/hooks/store";
import { useUser, useUserProfile, useUserSettings, useWorkspace } from "@/hooks/store";
type TPageType = EPageTypes;
@ -26,32 +26,16 @@ export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props
// props
const { children, pageType = EPageTypes.AUTHENTICATED } = props;
// hooks
const {
isLoading: isUserLoading,
data: currentUser,
currentUserSettings: { isLoading: currentUserSettingsLoader, data: currentUserSettings, fetchCurrentUserSettings },
profile: { isLoading: currentUserProfileLoader, data: currentUserProfile, fetchUserProfile },
fetchCurrentUser,
} = useUser();
const { loader: workspaceLoader, workspaces, fetchWorkspaces } = useWorkspace();
const { isLoading: isUserLoading, data: currentUser, fetchCurrentUser } = useUser();
const { data: currentUserProfile } = useUserProfile();
const { data: currentUserSettings } = useUserSettings();
const { workspaces } = useWorkspace();
useSWR("USER_INFORMATION", async () => fetchCurrentUser(), {
const { isLoading: isUserSWRLoading } = useSWR("USER_INFORMATION", async () => await fetchCurrentUser(), {
revalidateOnFocus: false,
shouldRetryOnError: false,
});
useSWR(
currentUser && currentUser?.id ? "USER_PROFILE_SETTINGS_INFORMATION" : null,
async () => {
if (currentUser && currentUser?.id) {
fetchCurrentUserSettings();
fetchUserProfile();
fetchWorkspaces();
}
},
{ revalidateOnFocus: false, shouldRetryOnError: false }
);
const getWorkspaceRedirectionUrl = (): string => {
let redirectionRoute = "/profile";
@ -75,7 +59,7 @@ export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props
return redirectionRoute;
};
if (isUserLoading || currentUserSettingsLoader || currentUserProfileLoader || workspaceLoader)
if (isUserSWRLoading || isUserLoading)
return (
<div className="relative flex h-screen w-full items-center justify-center">
<Spinner />
@ -87,13 +71,13 @@ export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props
if (pageType === EPageTypes.NON_AUTHENTICATED) {
if (!currentUser?.id) return <>{children}</>;
else {
if (currentUserProfile?.is_onboarded) {
if (currentUserProfile?.id && currentUserProfile?.is_onboarded) {
const currentRedirectRoute = getWorkspaceRedirectionUrl();
router.push(currentRedirectRoute);
return;
return <></>;
} else {
router.push("/onboarding");
return;
return <></>;
}
}
}
@ -101,26 +85,26 @@ export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props
if (pageType === EPageTypes.ONBOARDING) {
if (!currentUser?.id) {
router.push("/accounts/sign-in");
return;
return <></>;
} else {
if (currentUser && currentUserProfile?.is_onboarded) {
if (currentUser && currentUserProfile?.id && currentUserProfile?.is_onboarded) {
const currentRedirectRoute = getWorkspaceRedirectionUrl();
router.push(currentRedirectRoute);
return;
return <></>;
} else return <>{children}</>;
}
}
if (pageType === EPageTypes.AUTHENTICATED) {
if (currentUser?.id) {
if (currentUserProfile?.is_onboarded) return <>{children}</>;
if (currentUserProfile && currentUserProfile?.id && currentUserProfile?.is_onboarded) return <>{children}</>;
else {
router.push(`/onboarding`);
return;
return <></>;
}
} else {
router.push("/accounts/sign-in");
return;
return <></>;
}
}

View File

@ -21,7 +21,7 @@ const StoreWrapper: FC<TStoreWrapper> = observer((props) => {
const { setQuery } = useAppRouter();
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
const {
profile: { data: userProfile },
userProfile: { data: userProfile },
} = useUser();
// states
const [dom, setDom] = useState<undefined | HTMLElement>();

View File

@ -28,8 +28,8 @@ import { AuthenticationWrapper } from "@/lib/wrappers";
// services
import { AuthService } from "@/services/auth.service";
// images
import PlaneBackgroundPatternDark from "public/onboarding/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
type TForgotPasswordFormValues = {
@ -96,25 +96,25 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
};
return (
<div className="relative">
<div className="relative w-screen h-screen overflow-hidden">
<PageHead title="Forgot Password" />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-screen min-h-screen object-cover"
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10">
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
<div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
<div className="text-center text-sm font-medium text-onboarding-text-300">
New to Plane?{" "}
<Link
href="/accounts/sign-up"
href="/"
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
className="font-semibold text-custom-primary-100 hover:underline"
>
@ -122,66 +122,64 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
</Link>
</div>
</div>
<div className="mx-auto h-full">
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
<div className="mx-auto flex flex-col">
<div className="text-center space-y-1 py-4 mx-auto sm:w-96">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Reset your password
</h3>
<p className="font-medium text-onboarding-text-400">
Enter your user account{"'"}s verified email address and we will send you a password reset link.
</p>
</div>
<form onSubmit={handleSubmit(handleForgotPassword)} className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96">
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email
</label>
<Controller
control={control}
name="email"
rules={{
required: "Email is required",
validate: (value) => checkEmailValidity(value) || "Email is invalid",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.email)}
placeholder="name@company.com"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
disabled={resendTimerCode > 0}
/>
)}
/>
{resendTimerCode > 0 && (
<p className="flex w-full items-start px-1 gap-1 text-xs font-medium text-green-700">
<CircleCheck height={12} width={12} className="mt-0.5" />
We sent the reset link to your email address
</p>
)}
</div>
<Button
type="submit"
variant="primary"
className="w-full"
size="lg"
disabled={!isValid}
loading={isSubmitting || resendTimerCode > 0}
>
{resendTimerCode > 0 ? `Resend in ${resendTimerCode} seconds` : "Send reset link"}
</Button>
<Link href="/" className={cn("w-full", getButtonStyling("link-neutral", "lg"))}>
Back to sign in
</Link>
</form>
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
<div className="relative flex flex-col space-y-6">
<div className="text-center space-y-1 py-4">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Reset your password
</h3>
<p className="font-medium text-onboarding-text-400">
Enter your user account{"'"}s verified email address and we will send you a password reset link.
</p>
</div>
<form onSubmit={handleSubmit(handleForgotPassword)} className="mt-5 space-y-4">
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email
</label>
<Controller
control={control}
name="email"
rules={{
required: "Email is required",
validate: (value) => checkEmailValidity(value) || "Email is invalid",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.email)}
placeholder="name@company.com"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
disabled={resendTimerCode > 0}
/>
)}
/>
{resendTimerCode > 0 && (
<p className="flex w-full items-start px-1 gap-1 text-xs font-medium text-green-700">
<CircleCheck height={12} width={12} className="mt-0.5" />
We sent the reset link to your email address
</p>
)}
</div>
<Button
type="submit"
variant="primary"
className="w-full"
size="lg"
disabled={!isValid}
loading={isSubmitting || resendTimerCode > 0}
>
{resendTimerCode > 0 ? `Resend in ${resendTimerCode} seconds` : "Send reset link"}
</Button>
<Link href="/" className={cn("w-full", getButtonStyling("link-neutral", "lg"))}>
Back to sign in
</Link>
</form>
</div>
</div>
</div>

View File

@ -22,8 +22,8 @@ import { AuthenticationWrapper } from "@/lib/wrappers";
// services
import { AuthService } from "@/services/auth.service";
// images
import PlaneBackgroundPatternDark from "public/onboarding/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
type TResetPasswordFormValues = {
@ -74,120 +74,118 @@ const ResetPasswordPage: NextPageWithLayout = () => {
);
return (
<div className="relative">
<div className="relative w-screen h-screen overflow-hidden">
<PageHead title="Reset Password" />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-screen min-h-screen object-cover"
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10">
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
<div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
</div>
<div className="mx-auto h-full">
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
<div className="mx-auto flex flex-col">
<div className="text-center space-y-1 py-4 mx-auto sm:w-96">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Set new password
</h3>
<p className="font-medium text-onboarding-text-400">Secure your account with a strong password</p>
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
<div className="relative flex flex-col space-y-6">
<div className="text-center space-y-1 py-4">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Set new password
</h3>
<p className="font-medium text-onboarding-text-400">Secure your account with a strong password</p>
</div>
<form
className="mt-5 space-y-4"
method="POST"
action={`${API_BASE_URL}/auth/reset-password/${uidb64?.toString()}/${token?.toString()}/`}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={resetFormData.email}
//hasError={Boolean(errors.email)}
placeholder="name@company.com"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed"
disabled
/>
</div>
</div>
<form
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
method="POST"
action={`${API_BASE_URL}/auth/reset-password/${uidb64?.toString()}/${token?.toString()}/`}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={resetFormData.email}
//hasError={Boolean(errors.email)}
placeholder="name@company.com"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed"
disabled
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
Password
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
name="password"
value={resetFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
//hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoFocus
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
</div>
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
Password
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
name="password"
value={resetFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
//hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoFocus
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
{isPasswordInputFocused && <PasswordStrengthMeter password={resetFormData.password} />}
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
Confirm password
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
name="confirm_password"
value={resetFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
{!!resetFormData.confirm_password && resetFormData.password !== resetFormData.confirm_password && (
<span className="text-sm text-red-500">Passwords don{"'"}t match</span>
)}
</div>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
Set password
</Button>
</form>
</div>
{isPasswordInputFocused && <PasswordStrengthMeter password={resetFormData.password} />}
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
Confirm password
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
name="confirm_password"
value={resetFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
{!!resetFormData.confirm_password && resetFormData.password !== resetFormData.confirm_password && (
<span className="text-sm text-red-500">Passwords don{"'"}t match</span>
)}
</div>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
Set password
</Button>
</form>
</div>
</div>
</div>

View File

@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
import Image from "next/image";
import { useRouter } from "next/router";
// icons
import { useTheme } from "next-themes";
import { Eye, EyeOff } from "lucide-react";
// ui
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
@ -14,14 +15,16 @@ import { getPasswordStrength } from "@/helpers/password.helper";
// hooks
import { useUser } from "@/hooks/store";
// layouts
import { UserAuthWrapper } from "@/layouts/auth-layout";
import DefaultLayout from "@/layouts/default-layout";
// lib
import { NextPageWithLayout } from "@/lib/types";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers";
// services
import { AuthService } from "@/services/auth.service";
// images
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
type TResetPasswordFormValues = {
@ -50,6 +53,8 @@ const SetPasswordPage: NextPageWithLayout = observer(() => {
});
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
// hooks
const { resolvedTheme } = useTheme();
const { data: user } = useUser();
@ -90,112 +95,114 @@ const SetPasswordPage: NextPageWithLayout = observer(() => {
};
return (
<div className="relative">
<div className="relative w-screen h-screen overflow-hidden">
<PageHead title="Reset Password" />
<div className="absolute inset-0 z-0">
<Image src={PlaneBackgroundPattern} className="w-screen object-cover" alt="Plane background pattern" />
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10">
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
<div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
</div>
<div className="mx-auto h-full">
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
<div className="mx-auto flex flex-col">
<div className="text-center space-y-1 py-4 mx-auto sm:w-96">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Secure your account
</h3>
<p className="font-medium text-onboarding-text-400">Setting password helps you login securely</p>
</div>
<form className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96" onSubmit={(e) => handleSubmit(e)}>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={user?.email}
//hasError={Boolean(errors.email)}
placeholder="name@company.com"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed"
disabled
/>
</div>
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
Set a password
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
name="password"
value={passwordFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
//hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoFocus
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
{isPasswordInputFocused && <PasswordStrengthMeter password={passwordFormData.password} />}
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
Confirm password
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
name="confirm_password"
value={passwordFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
{!!passwordFormData.confirm_password &&
passwordFormData.password !== passwordFormData.confirm_password && (
<span className="text-sm text-red-500">Passwords don{"'"}t match</span>
)}
</div>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
Continue
</Button>
</form>
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
<div className="relative flex flex-col space-y-6">
<div className="text-center space-y-1 py-4">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Secure your account
</h3>
<p className="font-medium text-onboarding-text-400">Setting password helps you login securely</p>
</div>
<form className="mt-5 space-y-4" onSubmit={(e) => handleSubmit(e)}>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={user?.email}
//hasError={Boolean(errors.email)}
placeholder="name@company.com"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed"
disabled
/>
</div>
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
Set a password
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
name="password"
value={passwordFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
//hasError={Boolean(errors.password)}
placeholder="Enter password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoFocus
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
{isPasswordInputFocused && <PasswordStrengthMeter password={passwordFormData.password} />}
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
Confirm password
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword ? "text" : "password"}
name="confirm_password"
value={passwordFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
/>
{showPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(false)}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => setShowPassword(true)}
/>
)}
</div>
{!!passwordFormData.confirm_password &&
passwordFormData.password !== passwordFormData.confirm_password && (
<span className="text-sm text-red-500">Passwords don{"'"}t match</span>
)}
</div>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
Continue
</Button>
</form>
</div>
</div>
</div>
@ -205,9 +212,9 @@ const SetPasswordPage: NextPageWithLayout = observer(() => {
SetPasswordPage.getLayout = function getLayout(page: ReactElement) {
return (
<UserAuthWrapper>
<AuthenticationWrapper>
<DefaultLayout>{page}</DefaultLayout>
</UserAuthWrapper>
</AuthenticationWrapper>
);
};

View File

@ -19,8 +19,8 @@ import { NextPageWithLayout } from "@/lib/types";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers";
// assets
import PlaneBackgroundPatternDark from "public/onboarding/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
export type AuthType = "sign-in" | "sign-up";
@ -32,19 +32,19 @@ const SignInPage: NextPageWithLayout = observer(() => {
const { resolvedTheme } = useTheme();
return (
<div className="relative">
<div className="relative w-screen h-screen overflow-hidden">
<PageHead title="Sign In" />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-screen min-h-screen object-cover"
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10">
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
<div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
<div className="text-center text-sm font-medium text-onboarding-text-300">
@ -58,10 +58,8 @@ const SignInPage: NextPageWithLayout = observer(() => {
</Link>
</div>
</div>
<div className="mx-auto h-full">
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
<SignInAuthRoot />
</div>
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
<SignInAuthRoot />
</div>
</div>
</div>

View File

@ -10,11 +10,12 @@ import { PageHead } from "@/components/core";
import { CreateWorkspaceForm } from "@/components/workspace";
import { useUser } from "@/hooks/store";
// layouts
import { UserAuthWrapper } from "@/layouts/auth-layout";
import DefaultLayout from "@/layouts/default-layout";
// components
// images
import { NextPageWithLayout } from "@/lib/types";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers";
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
// types
@ -79,9 +80,9 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
CreateWorkspacePage.getLayout = function getLayout(page: ReactElement) {
return (
<UserAuthWrapper>
<AuthenticationWrapper>
<DefaultLayout>{page} </DefaultLayout>
</UserAuthWrapper>
</AuthenticationWrapper>
);
};

View File

@ -1,16 +1,68 @@
import { ReactElement } from "react";
import React, { ReactElement } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
// ui
import { useTheme } from "next-themes";
// components
import { SignUpView } from "@/components/page-views";
import { SignUpAuthRoot } from "@/components/account";
import { PageHead } from "@/components/core";
// constants
import { NAVIGATE_TO_SIGNIN } from "@/constants/event-tracker";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useEventTracker } from "@/hooks/store";
// layouts
import DefaultLayout from "@/layouts/default-layout";
// type
// types
import { NextPageWithLayout } from "@/lib/types";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers";
// assets
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
const HomePage: NextPageWithLayout = () => <SignUpView />;
const HomePage: NextPageWithLayout = observer(() => {
const { resolvedTheme } = useTheme();
// hooks
const { captureEvent } = useEventTracker();
return (
<div className="relative w-screen h-screen overflow-hidden">
<PageHead title="Sign Up" />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
<div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
<div className="text-center text-sm font-medium text-onboarding-text-300">
Already have an account?{" "}
<Link
href="/accounts/sign-in"
onClick={() => captureEvent(NAVIGATE_TO_SIGNIN, {})}
className="font-semibold text-custom-primary-100 hover:underline"
>
Sign In
</Link>
</div>
</div>
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
<SignUpAuthRoot />
</div>
</div>
</div>
);
});
HomePage.getLayout = function getLayout(page: ReactElement) {
return (

View File

@ -22,10 +22,11 @@ import { ROLE } from "@/constants/workspace";
import { truncateText } from "@/helpers/string.helper";
import { getUserRole } from "@/helpers/user.helper";
import { useEventTracker, useUser, useWorkspace } from "@/hooks/store";
import { UserAuthWrapper } from "@/layouts/auth-layout";
import DefaultLayout from "@/layouts/default-layout";
// types
import { NextPageWithLayout } from "@/lib/types";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers";
// services
import { UserService } from "@/services/user.service";
import { WorkspaceService } from "@/services/workspace.service";
@ -238,9 +239,9 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
UserInvitationsPage.getLayout = function getLayout(page: ReactElement) {
return (
<UserAuthWrapper>
<AuthenticationWrapper>
<DefaultLayout>{page}</DefaultLayout>
</UserAuthWrapper>
</AuthenticationWrapper>
);
};

View File

@ -12,14 +12,16 @@ import { InviteMembers, CreateOrJoinWorkspaces, ProfileSetup } from "@/component
// constants
import { USER_ONBOARDING_COMPLETED } from "@/constants/event-tracker";
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useUser, useWorkspace, useUserProfile, useEventTracker } from "@/hooks/store";
import useUserAuth from "@/hooks/use-user-auth";
// layouts
import { UserAuthWrapper } from "@/layouts/auth-layout";
import DefaultLayout from "@/layouts/default-layout";
// lib types
import { NextPageWithLayout } from "@/lib/types";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers";
// services
import { WorkspaceService } from "@/services/workspace.service";
@ -39,16 +41,10 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
const router = useRouter();
// store hooks
const { captureEvent } = useEventTracker();
const { data: user, isLoading: currentUserLoader, updateCurrentUser } = useUser();
const { data: user, updateCurrentUser } = useUser();
const { data: profile, updateUserOnBoard, updateUserProfile } = useUserProfile();
const { workspaces, fetchWorkspaces } = useWorkspace();
// custom hooks
const {} = useUserAuth({
routeAuth: "onboarding",
user: user || null,
userProfile: profile,
isLoading: currentUserLoader,
});
// computed values
const workspacesList = Object.values(workspaces ?? {});
// fetching workspaces list
@ -145,7 +141,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
handleStepChange();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, step, updateCurrentUser, workspacesList]);
}, [user, step, profile.onboarding_step, updateCurrentUser, workspacesList]);
return (
<>
@ -190,9 +186,9 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
OnboardingPage.getLayout = function getLayout(page: ReactElement) {
return (
<UserAuthWrapper>
<AuthenticationWrapper pageType={EPageTypes.ONBOARDING}>
<DefaultLayout>{page}</DefaultLayout>
</UserAuthWrapper>
</AuthenticationWrapper>
);
};

Some files were not shown because too many files have changed in this diff Show More