Merge branch 'chore-admin-file-structure' of gurusainath:makeplane/plane into chore-admin-file-structure
2
admin/.env.example
Normal file
@ -0,0 +1,2 @@
|
||||
NEXT_PUBLIC_APP_URL=
|
||||
NEXT_PUBLIC_API_BASE_URL=
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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)}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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
@ -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.";
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
@ -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"
|
||||
}
|
||||
|
13
admin/public/site.webmanifest.json
Normal 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" }
|
||||
]
|
||||
}
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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) => {
|
||||
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
@ -32,7 +32,6 @@ from plane.api.serializers import (
|
||||
LabelSerializer,
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
WorkspaceEntityPermission,
|
||||
ProjectEntityPermission,
|
||||
ProjectLitePermission,
|
||||
ProjectMemberPermission,
|
||||
|
@ -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()
|
||||
|
@ -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."
|
||||
)
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -1,3 +1,3 @@
|
||||
-r base.txt
|
||||
|
||||
# server
|
||||
gunicorn==22.0.0
|
||||
|
@ -1,4 +1,4 @@
|
||||
-r base.txt
|
||||
|
||||
# test checker
|
||||
pytest==7.1.2
|
||||
coverage==6.5.0
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -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";
|
||||
|
@ -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" />
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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) =>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
@ -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}>
|
||||
I’ll do it later
|
||||
|
@ -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>
|
||||
|
@ -1,2 +1 @@
|
||||
export * from "./signup";
|
||||
export * from "./workspace-dashboard";
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
@ -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;
|
||||
|
||||
|
@ -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" : ""
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
};
|
@ -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;
|
||||
};
|
3
web/hooks/store/user/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./user-user";
|
||||
export * from "./user-user-profile";
|
||||
export * from "./user-user-settings";
|
11
web/hooks/store/user/user-user-profile.ts
Normal 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;
|
||||
};
|
11
web/hooks/store/user/user-user-settings.ts
Normal 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;
|
||||
};
|
@ -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;
|
@ -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
|
||||
|
@ -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;
|
@ -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}</>;
|
||||
});
|
@ -1,4 +1,2 @@
|
||||
export * from "./user-wrapper";
|
||||
export * from "./workspace-wrapper";
|
||||
export * from "./project-wrapper";
|
||||
export * from "./admin-wrapper";
|
||||
|
@ -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}</>;
|
||||
});
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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 <></>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>();
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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 (
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|