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
|
// lib
|
||||||
import { StoreProvider } from "@/lib/store-context";
|
import { StoreProvider } from "@/lib/store-context";
|
||||||
import { AppWrapper } from "@/lib/wrappers";
|
import { AppWrapper } from "@/lib/wrappers";
|
||||||
|
// constants
|
||||||
|
import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_USER_NAME, SITE_KEYWORDS, SITE_TITLE } from "@/constants/seo";
|
||||||
// styles
|
// styles
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
@ -12,16 +14,35 @@ interface RootLayoutProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => (
|
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => {
|
||||||
<html lang="en">
|
const prefix = parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0") === 0 ? "/" : "/god-mode/";
|
||||||
<body className={`antialiased`}>
|
|
||||||
<StoreProvider {...pageProps}>
|
return (
|
||||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
<html lang="en">
|
||||||
<AppWrapper>{children}</AppWrapper>
|
<head>
|
||||||
</ThemeProvider>
|
<title>{SITE_TITLE}</title>
|
||||||
</StoreProvider>
|
<meta property="og:site_name" content={SITE_NAME} />
|
||||||
</body>
|
<meta property="og:title" content={SITE_TITLE} />
|
||||||
</html>
|
<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;
|
export default RootLayout;
|
||||||
|
@ -5,13 +5,13 @@ import { useSearchParams } from "next/navigation";
|
|||||||
// services
|
// services
|
||||||
import { AuthService } from "@/services/auth.service";
|
import { AuthService } from "@/services/auth.service";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input, Spinner } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { Banner } from "components/common";
|
import { Banner } from "components/common";
|
||||||
// icons
|
// icons
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL, cn } from "@/helpers/common.helper";
|
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||||
|
|
||||||
// service initialization
|
// service initialization
|
||||||
const authService = new AuthService();
|
const authService = new AuthService();
|
||||||
@ -52,6 +52,7 @@ export const InstanceSignInForm: FC = (props) => {
|
|||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||||
const [formData, setFormData] = useState<TFormData>(defaultFromData);
|
const [formData, setFormData] = useState<TFormData>(defaultFromData);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
|
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
|
||||||
setFormData((prev) => ({ ...prev, [key]: value }));
|
setFormData((prev) => ({ ...prev, [key]: value }));
|
||||||
@ -85,27 +86,40 @@ export const InstanceSignInForm: FC = (props) => {
|
|||||||
} else return { type: undefined, message: undefined };
|
} else return { type: undefined, message: undefined };
|
||||||
}, [errorCode, errorMessage]);
|
}, [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 (
|
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="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="w-full md:w-4/6 lg:w-3/6 xl:w-2/6 space-y-10">
|
<div className="relative flex flex-col space-y-6">
|
||||||
<div className="text-center space-y-1">
|
<div className="text-center space-y-1">
|
||||||
<h3 className="text-3xl font-bold">Manage your Plane instance</h3>
|
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
|
||||||
<p className="font-medium text-custom-text-400">Configure instance-wide settings to secure your instance</p>
|
Manage your Plane instance
|
||||||
|
</h3>
|
||||||
|
<p className="font-medium text-onboarding-text-400">
|
||||||
|
Configure instance-wide settings to secure your instance
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{errorData.type && errorData?.message && <Banner type="error" message={errorData?.message} />}
|
{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} />
|
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<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>
|
Email <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<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"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
@ -118,12 +132,12 @@ export const InstanceSignInForm: FC = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<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>
|
Password <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<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"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
@ -153,7 +167,7 @@ export const InstanceSignInForm: FC = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
|
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
|
||||||
Sign in
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Sign in"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -5,13 +5,13 @@ import { useSearchParams } from "next/navigation";
|
|||||||
// services
|
// services
|
||||||
import { AuthService } from "@/services/auth.service";
|
import { AuthService } from "@/services/auth.service";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Checkbox, Input } from "@plane/ui";
|
import { Button, Checkbox, Input, Spinner } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { Banner, PasswordStrengthMeter } from "components/common";
|
import { Banner, PasswordStrengthMeter } from "components/common";
|
||||||
// icons
|
// icons
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL, cn } from "@/helpers/common.helper";
|
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||||
import { getPasswordStrength } from "@/helpers/password.helper";
|
import { getPasswordStrength } from "@/helpers/password.helper";
|
||||||
|
|
||||||
// service initialization
|
// service initialization
|
||||||
@ -68,6 +68,7 @@ export const InstanceSignUpForm: FC = (props) => {
|
|||||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||||
const [formData, setFormData] = useState<TFormData>(defaultFromData);
|
const [formData, setFormData] = useState<TFormData>(defaultFromData);
|
||||||
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
|
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
|
||||||
setFormData((prev) => ({ ...prev, [key]: value }));
|
setFormData((prev) => ({ ...prev, [key]: value }));
|
||||||
@ -109,6 +110,7 @@ export const InstanceSignUpForm: FC = (props) => {
|
|||||||
|
|
||||||
const isButtonDisabled = useMemo(
|
const isButtonDisabled = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
!isSubmitting &&
|
||||||
formData.first_name &&
|
formData.first_name &&
|
||||||
formData.email &&
|
formData.email &&
|
||||||
formData.password &&
|
formData.password &&
|
||||||
@ -116,15 +118,19 @@ export const InstanceSignUpForm: FC = (props) => {
|
|||||||
formData.password === formData.confirm_password
|
formData.password === formData.confirm_password
|
||||||
? false
|
? false
|
||||||
: true,
|
: true,
|
||||||
[formData]
|
[formData.confirm_password, formData.email, formData.first_name, formData.password, isSubmitting]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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="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="w-full md:w-4/6 lg:w-3/6 xl:w-2/6 space-y-10">
|
<div className="relative flex flex-col space-y-6">
|
||||||
<div className="text-center space-y-1">
|
<div className="text-center space-y-1">
|
||||||
<h3 className="text-3xl font-bold">Setup your Plane Instance</h3>
|
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
|
||||||
<p className="font-medium text-custom-text-400">Post setup you will be able to manage this Plane instance.</p>
|
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>
|
</div>
|
||||||
|
|
||||||
{errorData.type &&
|
{errorData.type &&
|
||||||
@ -133,16 +139,22 @@ export const InstanceSignUpForm: FC = (props) => {
|
|||||||
<Banner type="error" message={errorData?.message} />
|
<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} />
|
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-full space-y-1">
|
<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>
|
First name <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<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"
|
id="first_name"
|
||||||
name="first_name"
|
name="first_name"
|
||||||
type="text"
|
type="text"
|
||||||
@ -154,11 +166,11 @@ export const InstanceSignUpForm: FC = (props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full space-y-1">
|
<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
|
Last name
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<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"
|
id="last_name"
|
||||||
name="last_name"
|
name="last_name"
|
||||||
type="text"
|
type="text"
|
||||||
@ -171,11 +183,11 @@ export const InstanceSignUpForm: FC = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<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>
|
Email <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<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"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
@ -191,11 +203,11 @@ export const InstanceSignUpForm: FC = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<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
|
Company name
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<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"
|
id="company_name"
|
||||||
name="company_name"
|
name="company_name"
|
||||||
type="text"
|
type="text"
|
||||||
@ -207,12 +219,12 @@ export const InstanceSignUpForm: FC = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<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>
|
Set a password <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<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"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type={showPassword ? "text" : "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">
|
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
|
||||||
Confirm password
|
Confirm password
|
||||||
</label>
|
</label>
|
||||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
|
id="confirm_password"
|
||||||
name="confirm_password"
|
name="confirm_password"
|
||||||
|
inputSize="md"
|
||||||
value={formData.confirm_password}
|
value={formData.confirm_password}
|
||||||
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
|
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
|
||||||
placeholder="Confirm password"
|
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 ? (
|
{showPassword ? (
|
||||||
<EyeOff
|
<button
|
||||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
type="button"
|
||||||
|
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||||
onClick={() => setShowPassword(false)}
|
onClick={() => setShowPassword(false)}
|
||||||
/>
|
>
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<Eye
|
<button
|
||||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
type="button"
|
||||||
|
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||||
onClick={() => setShowPassword(true)}
|
onClick={() => setShowPassword(true)}
|
||||||
/>
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!!formData.confirm_password && formData.password !== formData.confirm_password && (
|
{!!formData.confirm_password && formData.password !== formData.confirm_password && (
|
||||||
@ -288,7 +308,10 @@ export const InstanceSignUpForm: FC = (props) => {
|
|||||||
checked={formData.is_telemetry_enabled}
|
checked={formData.is_telemetry_enabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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.
|
Allow Plane to anonymously collect usage events.
|
||||||
</label>
|
</label>
|
||||||
<a href="https://docs.plane.so/telemetry" className="text-sm font-medium text-blue-500 hover:text-blue-600">
|
<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">
|
<div className="py-2">
|
||||||
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
|
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
|
||||||
Continue
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -3,11 +3,10 @@
|
|||||||
import { FC, useState, useRef } from "react";
|
import { FC, useState, useRef } from "react";
|
||||||
import { Transition } from "@headlessui/react";
|
import { Transition } from "@headlessui/react";
|
||||||
import Link from "next/link";
|
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
|
// hooks
|
||||||
import { useTheme } from "@/hooks";
|
import { useTheme } from "@/hooks";
|
||||||
// icons
|
|
||||||
import { DiscordIcon, GithubIcon } from "@plane/ui";
|
|
||||||
// assets
|
// assets
|
||||||
import packageJson from "package.json";
|
import packageJson from "package.json";
|
||||||
|
|
||||||
@ -37,18 +36,26 @@ export const HelpSection: FC = () => {
|
|||||||
// refs
|
// refs
|
||||||
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
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 (
|
return (
|
||||||
<div
|
<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 ${
|
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" : ""
|
isSidebarCollapsed ? "flex-col" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
|
||||||
className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full justify-end"}`}
|
<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
|
<button
|
||||||
type="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" : ""
|
isSidebarCollapsed ? "w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import { useTheme as useNextTheme } from "next-themes";
|
import { useTheme as useNextTheme } from "next-themes";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { LogOut, UserCog2, Palette } from "lucide-react";
|
import { LogOut, UserCog2, Palette } from "lucide-react";
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import { Avatar, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Avatar } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import { useTheme, useUser } from "@/hooks";
|
import { useTheme, useUser } from "@/hooks";
|
||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||||
|
// services
|
||||||
|
import { AuthService } from "@/services";
|
||||||
|
|
||||||
|
// service initialization
|
||||||
|
const authService = new AuthService();
|
||||||
|
|
||||||
export const SidebarDropdown = observer(() => {
|
export const SidebarDropdown = observer(() => {
|
||||||
// store hooks
|
// store hooks
|
||||||
@ -17,22 +22,21 @@ export const SidebarDropdown = observer(() => {
|
|||||||
const { currentUser, signOut } = useUser();
|
const { currentUser, signOut } = useUser();
|
||||||
// hooks
|
// hooks
|
||||||
const { resolvedTheme, setTheme } = useNextTheme();
|
const { resolvedTheme, setTheme } = useNextTheme();
|
||||||
|
// state
|
||||||
const handleSignOut = async () => {
|
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||||
await signOut().catch(() =>
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message: "Failed to sign out. Please try again.",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleThemeSwitch = () => {
|
const handleThemeSwitch = () => {
|
||||||
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
|
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
|
||||||
setTheme(newTheme);
|
setTheme(newTheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSignOut = () => signOut();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (csrfToken === undefined)
|
||||||
|
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||||
|
}, [csrfToken]);
|
||||||
|
|
||||||
return (
|
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="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">
|
<div className="h-full w-full truncate">
|
||||||
@ -93,12 +97,12 @@ export const SidebarDropdown = observer(() => {
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-2">
|
<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
|
<Menu.Item
|
||||||
as="button"
|
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"
|
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]" />
|
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||||
Sign out
|
Sign out
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Head from "next/head";
|
"use client";
|
||||||
|
|
||||||
type TPageHeader = {
|
type TPageHeader = {
|
||||||
title?: string;
|
title?: string;
|
||||||
@ -9,9 +9,9 @@ export const PageHeader: React.FC<TPageHeader> = (props) => {
|
|||||||
const { title = "God Mode - Plane", description = "Plane god mode" } = props;
|
const { title = "God Mode - Plane", description = "Plane god mode" } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Head>
|
<>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta name="description" content={description} />
|
<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="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="w-auto max-w-2xl relative space-y-8 py-10">
|
||||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
<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" />
|
<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
|
Get started by setting up your instance and workspace
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import Link from "next/link";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme as nextUseTheme } from "next-themes";
|
||||||
// ui
|
// ui
|
||||||
import { Button, getButtonStyling } from "@plane/ui";
|
import { Button, getButtonStyling } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -12,25 +11,19 @@ import { resolveGeneralTheme } from "helpers/common.helper";
|
|||||||
// icons
|
// icons
|
||||||
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
|
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
|
||||||
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
|
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
|
||||||
|
import { useTheme } from "@/hooks";
|
||||||
|
|
||||||
type Props = {
|
export const NewUserPopup: React.FC = observer(() => {
|
||||||
isOpen: boolean;
|
// hooks
|
||||||
onClose?: () => void;
|
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
|
||||||
};
|
|
||||||
|
|
||||||
export const CreateWorkspacePopup: React.FC<Props> = observer((props) => {
|
|
||||||
const { isOpen, onClose } = props;
|
|
||||||
// theme
|
// theme
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = nextUseTheme();
|
||||||
|
|
||||||
const handleClose = () => {
|
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/` : `/`}`}`;
|
||||||
onClose && onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
|
if (!isNewUserPopup) return <></>;
|
||||||
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="flex gap-4">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="text-base font-semibold">Create workspace</div>
|
<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.
|
workspace, you will need to login again.
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 pt-2">
|
<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
|
Create workspace
|
||||||
</Link>
|
</a>
|
||||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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";
|
import { FC, ReactNode } from "react";
|
||||||
|
// components
|
||||||
import { InstanceSidebar } from "@/components/admin-sidebar";
|
import { InstanceSidebar } from "@/components/admin-sidebar";
|
||||||
import { InstanceHeader } from "@/components/auth-header";
|
import { InstanceHeader } from "@/components/auth-header";
|
||||||
|
import { NewUserPopup } from "@/components/new-user-popup";
|
||||||
|
|
||||||
type TAdminLayout = {
|
type TAdminLayout = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -16,6 +18,7 @@ export const AdminLayout: FC<TAdminLayout> = (props) => {
|
|||||||
<InstanceHeader />
|
<InstanceHeader />
|
||||||
<div className="h-full w-full overflow-hidden">{children}</div>
|
<div className="h-full w-full overflow-hidden">{children}</div>
|
||||||
</main>
|
</main>
|
||||||
|
<NewUserPopup />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,25 +2,42 @@
|
|||||||
|
|
||||||
import { FC, ReactNode } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
import Image from "next/image";
|
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";
|
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||||
|
|
||||||
type TDefaultLayout = {
|
type TDefaultLayout = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
withoutBackground?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DefaultLayout: FC<TDefaultLayout> = (props) => {
|
export const DefaultLayout: FC<TDefaultLayout> = (props) => {
|
||||||
const { children } = props;
|
const { children, withoutBackground = false } = props;
|
||||||
|
// hooks
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full overflow-hidden overflow-y-auto flex flex-col">
|
<div className="relative">
|
||||||
<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="h-screen w-full overflow-hidden overflow-y-auto flex flex-col">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="container h-[110px] flex-shrink-0 mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
|
||||||
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
|
<div className="flex items-center gap-x-2 py-10">
|
||||||
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
|
||||||
|
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||||
|
</div>
|
||||||
</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>
|
||||||
<div className="w-full px-5 lg:px-0 mb-[100px] flex-grow">{children}</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -46,7 +46,7 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
|
|||||||
|
|
||||||
if (instance?.instance?.is_setup_done === false && authEnabled === "1")
|
if (instance?.instance?.is_setup_done === false && authEnabled === "1")
|
||||||
return (
|
return (
|
||||||
<DefaultLayout>
|
<DefaultLayout withoutBackground>
|
||||||
<InstanceNotReady />
|
<InstanceNotReady />
|
||||||
</DefaultLayout>
|
</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;
|
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 {
|
try {
|
||||||
if (this.instance === undefined) this.isLoading = true;
|
if (this.instance === undefined) this.isLoading = true;
|
||||||
const instance = await this.instanceService.getInstanceInfo();
|
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(() => {
|
runInAction(() => {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
|
@ -5,31 +5,41 @@ import { RootStore } from "@/store/root-store";
|
|||||||
type TTheme = "dark" | "light";
|
type TTheme = "dark" | "light";
|
||||||
export interface IThemeStore {
|
export interface IThemeStore {
|
||||||
// observables
|
// observables
|
||||||
|
isNewUserPopup: boolean;
|
||||||
theme: string | undefined;
|
theme: string | undefined;
|
||||||
isSidebarCollapsed: boolean | undefined;
|
isSidebarCollapsed: boolean | undefined;
|
||||||
// actions
|
// actions
|
||||||
|
toggleNewUserPopup: () => void;
|
||||||
toggleSidebar: (collapsed: boolean) => void;
|
toggleSidebar: (collapsed: boolean) => void;
|
||||||
setTheme: (currentTheme: TTheme) => void;
|
setTheme: (currentTheme: TTheme) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ThemeStore implements IThemeStore {
|
export class ThemeStore implements IThemeStore {
|
||||||
// observables
|
// observables
|
||||||
|
isNewUserPopup: boolean = false;
|
||||||
isSidebarCollapsed: boolean | undefined = undefined;
|
isSidebarCollapsed: boolean | undefined = undefined;
|
||||||
theme: string | undefined = undefined;
|
theme: string | undefined = undefined;
|
||||||
|
|
||||||
constructor(private store: RootStore) {
|
constructor(private store: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// observables
|
// observables
|
||||||
|
isNewUserPopup: observable.ref,
|
||||||
isSidebarCollapsed: observable.ref,
|
isSidebarCollapsed: observable.ref,
|
||||||
theme: observable.ref,
|
theme: observable.ref,
|
||||||
// action
|
// action
|
||||||
|
toggleNewUserPopup: action,
|
||||||
toggleSidebar: action,
|
toggleSidebar: action,
|
||||||
setTheme: 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
|
* @param isCollapsed
|
||||||
*/
|
*/
|
||||||
toggleSidebar = (isCollapsed: boolean) => {
|
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
|
* @param currentTheme
|
||||||
*/
|
*/
|
||||||
setTheme = async (currentTheme: TTheme) => {
|
setTheme = async (currentTheme: TTheme) => {
|
||||||
|
@ -3,11 +3,10 @@ import { IUser } from "@plane/types";
|
|||||||
// helpers
|
// helpers
|
||||||
import { EUserStatus, TUserStatus } from "@/helpers";
|
import { EUserStatus, TUserStatus } from "@/helpers";
|
||||||
// services
|
// services
|
||||||
import { UserService } from "services/user.service";
|
import { UserService } from "@/services/user.service";
|
||||||
// root store
|
// root store
|
||||||
import { RootStore } from "@/store/root-store";
|
import { RootStore } from "@/store/root-store";
|
||||||
import { AuthService } from "@/services";
|
import { AuthService } from "@/services";
|
||||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
|
||||||
|
|
||||||
export interface IUserStore {
|
export interface IUserStore {
|
||||||
// observables
|
// observables
|
||||||
@ -79,7 +78,6 @@ export class UserStore implements IUserStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
signOut = async () => {
|
signOut = async () => {
|
||||||
await this.authService.signOut(API_BASE_URL);
|
|
||||||
this.rootStore.resetOnSignOut();
|
this.rootStore.resetOnSignOut();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,6 @@ from plane.api.serializers import (
|
|||||||
LabelSerializer,
|
LabelSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
WorkspaceEntityPermission,
|
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
ProjectLitePermission,
|
ProjectLitePermission,
|
||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
|
@ -38,11 +38,13 @@ class SessionMiddleware(MiddlewareMixin):
|
|||||||
return response
|
return response
|
||||||
# First check if we need to delete this cookie.
|
# First check if we need to delete this cookie.
|
||||||
# The session should be deleted only if the session is entirely empty.
|
# The session should be deleted only if the session is entirely empty.
|
||||||
|
is_admin_path = "instances" in request.path
|
||||||
cookie_name = (
|
cookie_name = (
|
||||||
settings.ADMIN_SESSION_COOKIE_NAME
|
settings.ADMIN_SESSION_COOKIE_NAME
|
||||||
if "instances" in request.path
|
if is_admin_path
|
||||||
else settings.SESSION_COOKIE_NAME
|
else settings.SESSION_COOKIE_NAME
|
||||||
)
|
)
|
||||||
|
|
||||||
if cookie_name in request.COOKIES and empty:
|
if cookie_name in request.COOKIES and empty:
|
||||||
response.delete_cookie(
|
response.delete_cookie(
|
||||||
cookie_name,
|
cookie_name,
|
||||||
@ -59,11 +61,16 @@ class SessionMiddleware(MiddlewareMixin):
|
|||||||
max_age = None
|
max_age = None
|
||||||
expires = None
|
expires = None
|
||||||
else:
|
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_time = time.time() + max_age
|
||||||
expires = http_date(expires_time)
|
expires = http_date(expires_time)
|
||||||
|
|
||||||
# Save the session data and refresh the client cookie.
|
# Save the session data and refresh the client cookie.
|
||||||
# Skip session save for 5xx responses.
|
|
||||||
if response.status_code < 500:
|
if response.status_code < 500:
|
||||||
try:
|
try:
|
||||||
request.session.save()
|
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(
|
raise ImproperlyConfigured(
|
||||||
"SMTP is not configured. Please contact the support team."
|
"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(
|
return Response(
|
||||||
{
|
{
|
||||||
"error_code": "SMTP_NOT_CONFIGURED",
|
"error_code": "SMTP_NOT_CONFIGURED",
|
||||||
|
@ -150,8 +150,6 @@ class InstanceEndpoint(BaseAPIView):
|
|||||||
# is smtp configured
|
# is smtp configured
|
||||||
data["is_smtp_configured"] = (
|
data["is_smtp_configured"] = (
|
||||||
bool(EMAIL_HOST)
|
bool(EMAIL_HOST)
|
||||||
and bool(EMAIL_HOST_USER)
|
|
||||||
and bool(EMAIL_HOST_PASSWORD)
|
|
||||||
)
|
)
|
||||||
instance_data = serializer.data
|
instance_data = serializer.data
|
||||||
instance_data["workspaces_exist"] = Workspace.objects.count() > 1
|
instance_data["workspaces_exist"] = Workspace.objects.count() > 1
|
||||||
|
@ -333,6 +333,7 @@ SESSION_SAVE_EVERY_REQUEST = True
|
|||||||
|
|
||||||
# Admin Cookie
|
# Admin Cookie
|
||||||
ADMIN_SESSION_COOKIE_NAME = "plane-admin-session-id"
|
ADMIN_SESSION_COOKIE_NAME = "plane-admin-session-id"
|
||||||
|
ADMIN_SESSION_COOKIE_AGE = 3600
|
||||||
|
|
||||||
# CSRF cookies
|
# CSRF cookies
|
||||||
CSRF_COOKIE_SECURE = secure_origins
|
CSRF_COOKIE_SECURE = secure_origins
|
||||||
|
@ -1,37 +1,63 @@
|
|||||||
# base requirements
|
# base requirements
|
||||||
|
|
||||||
|
# django
|
||||||
Django==4.2.11
|
Django==4.2.11
|
||||||
psycopg==3.1.12
|
# rest framework
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.15.1
|
||||||
redis==4.6.0
|
# postgres
|
||||||
django-cors-headers==4.2.0
|
psycopg==3.1.18
|
||||||
whitenoise==6.5.0
|
psycopg-binary==3.1.18
|
||||||
django-allauth==0.55.2
|
psycopg-c==3.1.18
|
||||||
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
|
|
||||||
dj-database-url==2.1.0
|
dj-database-url==2.1.0
|
||||||
posthog==3.0.2
|
# redis
|
||||||
cryptography==42.0.4
|
redis==5.0.4
|
||||||
lxml==4.9.3
|
django-redis==5.4.0
|
||||||
boto3==1.28.40
|
# 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
|
zxcvbn==4.4.28
|
||||||
|
# timezone
|
||||||
|
pytz==2024.1
|
||||||
|
# jwt
|
||||||
|
jwt==1.3.1
|
@ -1,3 +1,5 @@
|
|||||||
-r base.txt
|
-r base.txt
|
||||||
|
# debug toolbar
|
||||||
django-debug-toolbar==4.1.0
|
django-debug-toolbar==4.3.0
|
||||||
|
# formatter
|
||||||
|
ruff==0.4.2
|
@ -1,3 +1,3 @@
|
|||||||
-r base.txt
|
-r base.txt
|
||||||
|
# server
|
||||||
gunicorn==22.0.0
|
gunicorn==22.0.0
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
-r base.txt
|
-r base.txt
|
||||||
|
# test checker
|
||||||
pytest==7.1.2
|
pytest==7.1.2
|
||||||
coverage==6.5.0
|
coverage==6.5.0
|
@ -3,11 +3,11 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
// icons
|
// icons
|
||||||
import { XCircle, CircleAlert } from "lucide-react";
|
import { XCircle, CircleAlert } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input, Spinner } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { checkEmailValidity } from "@/helpers/string.helper";
|
import { checkEmailValidity } from "@/helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IEmailCheckData } from "types/auth";
|
import { IEmailCheckData } from "@/types/auth";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSubmit: (data: IEmailCheckData) => Promise<void>;
|
onSubmit: (data: IEmailCheckData) => Promise<void>;
|
||||||
@ -40,7 +40,7 @@ export const EmailForm: React.FC<Props> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<div className="space-y-1">
|
||||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
||||||
Email
|
Email
|
||||||
@ -84,8 +84,8 @@ export const EmailForm: React.FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={!isValid} loading={isSubmitting}>
|
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={!isValid || isSubmitting}>
|
||||||
Continue
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,7 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Eye, EyeOff, XCircle } from "lucide-react";
|
import { Eye, EyeOff, XCircle } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input, Spinner } from "@plane/ui";
|
||||||
import { EAuthModes, EAuthSteps, ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/accounts";
|
import { EAuthModes, EAuthSteps, ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/accounts";
|
||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
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 [showPassword, setShowPassword] = useState(false);
|
||||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||||
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
instanceStore: { instance },
|
instanceStore: { instance },
|
||||||
@ -82,6 +83,7 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const isButtonDisabled = useMemo(
|
const isButtonDisabled = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
!isSubmitting &&
|
||||||
!!passwordFormData.password &&
|
!!passwordFormData.password &&
|
||||||
(mode === EAuthModes.SIGN_UP
|
(mode === EAuthModes.SIGN_UP
|
||||||
? getPasswordStrength(passwordFormData.password) >= 3 &&
|
? getPasswordStrength(passwordFormData.password) >= 3 &&
|
||||||
@ -89,14 +91,16 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
: true)
|
: true)
|
||||||
? false
|
? false
|
||||||
: true,
|
: true,
|
||||||
[mode, passwordFormData]
|
[isSubmitting, mode, passwordFormData.confirm_password, passwordFormData.password]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
|
className="mt-5 space-y-4"
|
||||||
method="POST"
|
method="POST"
|
||||||
action={`${API_BASE_URL}/auth/spaces/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`}
|
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="csrfmiddlewaretoken" value={csrfToken} />
|
||||||
<input type="hidden" name="next_path" value={next_path} />
|
<input type="hidden" name="next_path" value={next_path} />
|
||||||
@ -153,7 +157,7 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
{passwordSupport}
|
{passwordSupport}
|
||||||
</div>
|
</div>
|
||||||
{mode === EAuthModes.SIGN_UP && getPasswordStrength(passwordFormData.password) >= 3 && (
|
{mode === EAuthModes.SIGN_UP && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
|
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
|
||||||
Confirm password
|
Confirm password
|
||||||
@ -188,7 +192,7 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
{mode === EAuthModes.SIGN_IN ? (
|
{mode === EAuthModes.SIGN_IN ? (
|
||||||
<>
|
<>
|
||||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
<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>
|
</Button>
|
||||||
{instance && isSmtpConfigured && (
|
{instance && isSmtpConfigured && (
|
||||||
<Button
|
<Button
|
||||||
@ -204,7 +208,7 @@ export const PasswordForm: React.FC<Props> = (props) => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||||
Create account
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Create account"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -117,44 +117,42 @@ export const AuthRoot = observer(() => {
|
|||||||
const isOAuthEnabled =
|
const isOAuthEnabled =
|
||||||
instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled);
|
instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled);
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="relative flex flex-col space-y-6">
|
||||||
<div className="mx-auto flex flex-col">
|
<div className="space-y-1 text-center">
|
||||||
<div className="text-center space-y-1 py-4 mx-auto sm:w-96">
|
<h3 className="text-3xl font-bold text-onboarding-text-100">{header}</h3>
|
||||||
<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>
|
||||||
<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>
|
</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 />}
|
{isOAuthEnabled && <OAuthOptions />}
|
||||||
<TermsAndConditions mode={authMode} />
|
<TermsAndConditions mode={authMode} />
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
// hooks
|
|
||||||
import useTimer from "hooks/use-timer";
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// types
|
|
||||||
import { IEmailCheckData } from "types/auth";
|
|
||||||
// icons
|
// icons
|
||||||
import { CircleCheck, XCircle } from "lucide-react";
|
import { CircleCheck, XCircle } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input, Spinner } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||||
|
// hooks
|
||||||
|
import useTimer from "@/hooks/use-timer";
|
||||||
|
import useToast from "@/hooks/use-toast";
|
||||||
// services
|
// services
|
||||||
import { AuthService } from "@/services/authentication.service";
|
import { AuthService } from "@/services/authentication.service";
|
||||||
|
// types
|
||||||
|
import { IEmailCheckData } from "@/types/auth";
|
||||||
import { EAuthModes } from "./root";
|
import { EAuthModes } from "./root";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -41,6 +41,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
const [uniqueCodeFormData, setUniqueCodeFormData] = useState<TUniqueCodeFormValues>({ ...defaultValues, email });
|
const [uniqueCodeFormData, setUniqueCodeFormData] = useState<TUniqueCodeFormValues>({ ...defaultValues, email });
|
||||||
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
||||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { next_path } = router.query;
|
const { next_path } = router.query;
|
||||||
@ -99,12 +100,15 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0;
|
const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0;
|
||||||
|
const isButtonDisabled = isRequestingNewCode || !uniqueCodeFormData.code || isSubmitting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
|
className="mt-5 space-y-4"
|
||||||
method="POST"
|
method="POST"
|
||||||
action={`${API_BASE_URL}/auth/spaces/${mode === EAuthModes.SIGN_IN ? "magic-sign-in" : "magic-sign-up"}/`}
|
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="csrfmiddlewaretoken" value={csrfToken} />
|
||||||
<input type="hidden" name="next_path" value={next_path} />
|
<input type="hidden" name="next_path" value={next_path} />
|
||||||
@ -170,15 +174,14 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||||
type="submit"
|
{isRequestingNewCode ? (
|
||||||
variant="primary"
|
"Sending code"
|
||||||
className="w-full"
|
) : isSubmitting ? (
|
||||||
size="lg"
|
<Spinner height="20px" width="20px" />
|
||||||
loading={isRequestingNewCode}
|
) : (
|
||||||
disabled={isRequestingNewCode || !uniqueCodeFormData.code}
|
submitButtonText
|
||||||
>
|
)}
|
||||||
{isRequestingNewCode ? "Sending code" : submitButtonText}
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
// types
|
// types
|
||||||
import { IUser } from "@plane/types";
|
import { IUser } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { UserImageUploadModal } from "@/components/accounts";
|
import { UserImageUploadModal } from "@/components/accounts";
|
||||||
// hooks
|
// 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 (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="w-full mx-auto mt-2 space-y-4 sm:w-96">
|
<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>}
|
{errors.last_name && <span className="text-sm text-red-500">{errors.last_name.message}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button variant="primary" type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
|
||||||
variant="primary"
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||||
type="submit"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
disabled={isButtonDisabled}
|
|
||||||
loading={isSubmitting}
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -11,8 +11,8 @@ import { AuthRoot, UserLoggedIn } from "@/components/accounts";
|
|||||||
import useAuthRedirection from "@/hooks/use-auth-redirection";
|
import useAuthRedirection from "@/hooks/use-auth-redirection";
|
||||||
import { useMobxStore } from "@/lib/mobx/store-provider";
|
import { useMobxStore } from "@/lib/mobx/store-provider";
|
||||||
// images
|
// images
|
||||||
import PlaneBackgroundPatternDark from "public/onboarding/background-pattern-dark.svg";
|
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
|
||||||
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
|
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
|
||||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text-new.png";
|
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text-new.png";
|
||||||
|
|
||||||
export const AuthView = observer(() => {
|
export const AuthView = observer(() => {
|
||||||
@ -46,25 +46,23 @@ export const AuthView = observer(() => {
|
|||||||
{currentUser ? (
|
{currentUser ? (
|
||||||
<UserLoggedIn />
|
<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">
|
<div className="absolute inset-0 z-0">
|
||||||
<Image
|
<Image
|
||||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||||
className="w-screen min-h-screen object-cover"
|
className="w-full h-full object-cover"
|
||||||
alt="Plane background pattern"
|
alt="Plane background pattern"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative z-10">
|
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
|
<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">
|
<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>
|
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto h-full">
|
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
|
||||||
<div className="h-full overflow-auto px-7 pt-4 sm:px-0">
|
<AuthRoot />
|
||||||
<AuthRoot />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-dropzone": "^14.2.3",
|
||||||
"react-hook-form": "^7.38.0",
|
"react-hook-form": "^7.38.0",
|
||||||
"swr": "^2.2.2",
|
"swr": "^2.2.2",
|
||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
|
@ -17,8 +17,8 @@ import useTimer from "@/hooks/use-timer";
|
|||||||
// services
|
// services
|
||||||
import { AuthService } from "@/services/authentication.service";
|
import { AuthService } from "@/services/authentication.service";
|
||||||
// images
|
// images
|
||||||
import PlaneBackgroundPatternDark from "public/onboarding/background-pattern-dark.svg";
|
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
|
||||||
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
|
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
|
||||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||||
|
|
||||||
type TForgotPasswordFormValues = {
|
type TForgotPasswordFormValues = {
|
||||||
@ -79,9 +79,9 @@ const ForgotPasswordPage: NextPage = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="relative h-screen w-full overflow-hidden">
|
<div className="relative h-screen w-full overflow-hidden">
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<Image
|
<Image
|
||||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||||
className="w-screen min-h-screen object-cover"
|
className="w-full h-full object-cover"
|
||||||
alt="Plane background pattern"
|
alt="Plane background pattern"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -159,5 +159,4 @@ const ForgotPasswordPage: NextPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default ForgotPasswordPage;
|
export default ForgotPasswordPage;
|
||||||
|
@ -16,8 +16,8 @@ import { getPasswordStrength } from "@/helpers/password.helper";
|
|||||||
// services
|
// services
|
||||||
import { AuthService } from "@/services/authentication.service";
|
import { AuthService } from "@/services/authentication.service";
|
||||||
// images
|
// images
|
||||||
import PlaneBackgroundPatternDark from "public/onboarding/background-pattern-dark.svg";
|
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
|
||||||
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
|
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
|
||||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||||
|
|
||||||
type TResetPasswordFormValues = {
|
type TResetPasswordFormValues = {
|
||||||
@ -79,7 +79,7 @@ const ResetPasswordPage: NextPage = () => {
|
|||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<Image
|
<Image
|
||||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||||
className="w-screen min-h-screen object-cover"
|
className="w-full h-full object-cover"
|
||||||
alt="Plane background pattern"
|
alt="Plane background pattern"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
|
||||||
|
|
||||||
const OnBoardingPage = () => {
|
const OnBoardingPage = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { next_path } = router.query;
|
const { next_path } = router.query;
|
||||||
@ -30,7 +30,7 @@ const OnBoardingPage = () => {
|
|||||||
const user = userStore?.currentUser;
|
const user = userStore?.currentUser;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
return;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// complete onboarding
|
// complete onboarding
|
||||||
@ -117,6 +117,6 @@ const OnBoardingPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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": [
|
"globalEnv": [
|
||||||
"NODE_ENV",
|
"NODE_ENV",
|
||||||
"NEXT_PUBLIC_API_BASE_URL",
|
"NEXT_PUBLIC_API_BASE_URL",
|
||||||
|
"NEXT_PUBLIC_APP_URL",
|
||||||
"NEXT_PUBLIC_DEPLOY_URL",
|
"NEXT_PUBLIC_DEPLOY_URL",
|
||||||
"NEXT_PUBLIC_GOD_MODE_URL",
|
"NEXT_PUBLIC_GOD_MODE_URL",
|
||||||
"NEXT_PUBLIC_SENTRY_DSN",
|
"NEXT_PUBLIC_SENTRY_DSN",
|
||||||
|
@ -13,7 +13,7 @@ export const AuthBanner: FC<TAuthBanner> = (props) => {
|
|||||||
|
|
||||||
if (!bannerData) return <></>;
|
if (!bannerData) return <></>;
|
||||||
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">
|
<div className="w-4 h-4 flex-shrink-0 relative flex justify-center items-center">
|
||||||
<Info size={16} className="text-custom-primary-100" />
|
<Info size={16} className="text-custom-primary-100" />
|
||||||
</div>
|
</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 { IWorkspaceMemberInvitation } from "@plane/types";
|
||||||
|
import { Spinner } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { WorkspaceLogo } from "@/components/workspace/logo";
|
import { WorkspaceLogo } from "@/components/workspace/logo";
|
||||||
// helpers
|
// helpers
|
||||||
@ -13,7 +15,7 @@ type TAuthHeader = {
|
|||||||
invitationEmail: string | undefined;
|
invitationEmail: string | undefined;
|
||||||
authMode: EAuthModes;
|
authMode: EAuthModes;
|
||||||
currentAuthStep: EAuthSteps;
|
currentAuthStep: EAuthSteps;
|
||||||
handleLoader: (isLoading: boolean) => void;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Titles = {
|
const Titles = {
|
||||||
@ -50,9 +52,16 @@ const Titles = {
|
|||||||
const workSpaceService = new WorkspaceService();
|
const workSpaceService = new WorkspaceService();
|
||||||
|
|
||||||
export const AuthHeader: FC<TAuthHeader> = (props) => {
|
export const AuthHeader: FC<TAuthHeader> = (props) => {
|
||||||
const { workspaceSlug, invitationId, invitationEmail, authMode, currentAuthStep, handleLoader } = props;
|
const { workspaceSlug, invitationId, invitationEmail, authMode, currentAuthStep, children } = props;
|
||||||
// state
|
|
||||||
const [invitation, setInvitation] = useState<IWorkspaceMemberInvitation | undefined>(undefined);
|
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 = (
|
const getHeaderSubHeader = (
|
||||||
step: EAuthSteps,
|
step: EAuthSteps,
|
||||||
@ -64,9 +73,10 @@ export const AuthHeader: FC<TAuthHeader> = (props) => {
|
|||||||
const workspace = invitation.workspace;
|
const workspace = invitation.workspace;
|
||||||
return {
|
return {
|
||||||
header: (
|
header: (
|
||||||
<>
|
<div className="relative inline-flex items-center gap-2">
|
||||||
Join <WorkspaceLogo logo={workspace?.logo} name={workspace?.name} classNames="w-8 h-9" /> {workspace.name}
|
Join <WorkspaceLogo logo={workspace?.logo} name={workspace?.name} classNames="w-8 h-9 flex-shrink-0" />{" "}
|
||||||
</>
|
{workspace.name}
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
subHeader: `${
|
subHeader: `${
|
||||||
mode == EAuthModes.SIGN_UP ? "Create an account" : "Sign in"
|
mode == EAuthModes.SIGN_UP ? "Create an account" : "Sign in"
|
||||||
@ -77,23 +87,22 @@ export const AuthHeader: FC<TAuthHeader> = (props) => {
|
|||||||
return Titles[mode][step];
|
return Titles[mode][step];
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const { header, subHeader } = getHeaderSubHeader(currentAuthStep, authMode, invitation || undefined, invitationEmail);
|
||||||
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, invitationEmail);
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1 text-center">
|
<>
|
||||||
<h3 className="text-3xl font-bold text-onboarding-text-100">{header}</h3>
|
<div className="space-y-1 text-center">
|
||||||
<p className="font-medium text-onboarding-text-400">{subHeader}</p>
|
<h3 className="text-3xl font-bold text-onboarding-text-100">{header}</h3>
|
||||||
</div>
|
<p className="font-medium text-onboarding-text-400">{subHeader}</p>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ import { CircleAlert, XCircle } from "lucide-react";
|
|||||||
// types
|
// types
|
||||||
import { IEmailCheckData } from "@plane/types";
|
import { IEmailCheckData } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input, Spinner } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { checkEmailValidity } from "@/helpers/string.helper";
|
import { checkEmailValidity } from "@/helpers/string.helper";
|
||||||
|
|
||||||
@ -35,8 +35,10 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
|
|||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isButtonDisabled = email.length === 0 || Boolean(emailError?.email) || isSubmitting;
|
||||||
|
|
||||||
return (
|
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">
|
<div className="space-y-1">
|
||||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
||||||
Email
|
Email
|
||||||
@ -67,15 +69,8 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||||
type="submit"
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||||
variant="primary"
|
|
||||||
className="w-full"
|
|
||||||
size="lg"
|
|
||||||
disabled={email.length === 0 || Boolean(emailError?.email)}
|
|
||||||
loading={isSubmitting}
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,7 @@ import Link from "next/link";
|
|||||||
// icons
|
// icons
|
||||||
import { Eye, EyeOff, XCircle } from "lucide-react";
|
import { Eye, EyeOff, XCircle } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input, Spinner } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/account";
|
import { ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/account";
|
||||||
// constants
|
// constants
|
||||||
@ -45,6 +45,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||||
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
// hooks
|
// hooks
|
||||||
const { instance } = useInstance();
|
const { instance } = useInstance();
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
@ -84,6 +85,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||||||
|
|
||||||
const isButtonDisabled = useMemo(
|
const isButtonDisabled = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
!isSubmitting &&
|
||||||
!!passwordFormData.password &&
|
!!passwordFormData.password &&
|
||||||
(mode === EAuthModes.SIGN_UP
|
(mode === EAuthModes.SIGN_UP
|
||||||
? getPasswordStrength(passwordFormData.password) >= 3 &&
|
? getPasswordStrength(passwordFormData.password) >= 3 &&
|
||||||
@ -91,14 +93,16 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||||||
: true)
|
: true)
|
||||||
? false
|
? false
|
||||||
: true,
|
: true,
|
||||||
[mode, passwordFormData]
|
[isSubmitting, mode, passwordFormData.confirm_password, passwordFormData.password]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
|
className="mt-5 space-y-4"
|
||||||
method="POST"
|
method="POST"
|
||||||
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`}
|
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} />
|
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@ -189,7 +193,13 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
|||||||
{mode === EAuthModes.SIGN_IN ? (
|
{mode === EAuthModes.SIGN_IN ? (
|
||||||
<>
|
<>
|
||||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
<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>
|
</Button>
|
||||||
{instance && isSmtpConfigured && (
|
{instance && isSmtpConfigured && (
|
||||||
<Button
|
<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}>
|
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||||
Create account
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Create account"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { IEmailCheckData } from "@plane/types";
|
import { IEmailCheckData } from "@plane/types";
|
||||||
import { Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
AuthHeader,
|
AuthHeader,
|
||||||
@ -11,7 +11,7 @@ import {
|
|||||||
AuthPasswordForm,
|
AuthPasswordForm,
|
||||||
OAuthOptions,
|
OAuthOptions,
|
||||||
TermsAndConditions,
|
TermsAndConditions,
|
||||||
UniqueCodeForm,
|
AuthUniqueCodeForm,
|
||||||
} from "@/components/account";
|
} from "@/components/account";
|
||||||
// helpers
|
// helpers
|
||||||
import {
|
import {
|
||||||
@ -36,7 +36,6 @@ export const SignInAuthRoot = observer(() => {
|
|||||||
// states
|
// states
|
||||||
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
||||||
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
|
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
|
||||||
// hooks
|
// hooks
|
||||||
const { instance } = useInstance();
|
const { instance } = useInstance();
|
||||||
@ -74,30 +73,21 @@ export const SignInAuthRoot = observer(() => {
|
|||||||
const isOAuthEnabled =
|
const isOAuthEnabled =
|
||||||
instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled);
|
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 (
|
return (
|
||||||
<>
|
<div className="relative flex flex-col space-y-6">
|
||||||
<div className="relative max-w-lg mx-auto flex flex-col space-y-6">
|
<AuthHeader
|
||||||
<AuthHeader
|
workspaceSlug={workspaceSlug?.toString() || undefined}
|
||||||
workspaceSlug={workspaceSlug?.toString() || undefined}
|
invitationId={invitation_id?.toString() || undefined}
|
||||||
invitationId={invitation_id?.toString() || undefined}
|
invitationEmail={email || undefined}
|
||||||
invitationEmail={email || undefined}
|
authMode={EAuthModes.SIGN_IN}
|
||||||
authMode={EAuthModes.SIGN_IN}
|
currentAuthStep={authStep}
|
||||||
currentAuthStep={authStep}
|
>
|
||||||
handleLoader={setIsLoading}
|
|
||||||
/>
|
|
||||||
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
|
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
|
||||||
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
|
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
|
||||||
)}
|
)}
|
||||||
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
|
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
|
||||||
{authStep === EAuthSteps.UNIQUE_CODE && (
|
{authStep === EAuthSteps.UNIQUE_CODE && (
|
||||||
<UniqueCodeForm
|
<AuthUniqueCodeForm
|
||||||
email={email}
|
email={email}
|
||||||
handleEmailClear={() => {
|
handleEmailClear={() => {
|
||||||
setEmail("");
|
setEmail("");
|
||||||
@ -120,7 +110,7 @@ export const SignInAuthRoot = observer(() => {
|
|||||||
)}
|
)}
|
||||||
{isOAuthEnabled && <OAuthOptions />}
|
{isOAuthEnabled && <OAuthOptions />}
|
||||||
<TermsAndConditions isSignUp={false} />
|
<TermsAndConditions isSignUp={false} />
|
||||||
</div>
|
</AuthHeader>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
|||||||
// types
|
// types
|
||||||
import { IEmailCheckData } from "@plane/types";
|
import { IEmailCheckData } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
AuthHeader,
|
AuthHeader,
|
||||||
@ -39,7 +39,6 @@ export const SignUpAuthRoot: FC = observer(() => {
|
|||||||
// states
|
// states
|
||||||
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
||||||
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
|
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
|
||||||
// hooks
|
// hooks
|
||||||
const { instance } = useInstance();
|
const { instance } = useInstance();
|
||||||
@ -63,7 +62,7 @@ export const SignUpAuthRoot: FC = observer(() => {
|
|||||||
await authService
|
await authService
|
||||||
.signUpEmailCheck(data)
|
.signUpEmailCheck(data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (isSmtpConfigured) setAuthStep(EAuthSteps.PASSWORD);
|
if (isSmtpConfigured) setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||||
else setAuthStep(EAuthSteps.PASSWORD);
|
else setAuthStep(EAuthSteps.PASSWORD);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -83,51 +82,44 @@ export const SignUpAuthRoot: FC = observer(() => {
|
|||||||
const isOAuthEnabled =
|
const isOAuthEnabled =
|
||||||
instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled);
|
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 (
|
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
|
<AuthHeader
|
||||||
workspaceSlug={workspaceSlug?.toString() || undefined}
|
workspaceSlug={workspaceSlug?.toString() || undefined}
|
||||||
invitationId={invitation_id?.toString() || undefined}
|
invitationId={invitation_id?.toString() || undefined}
|
||||||
invitationEmail={email || undefined}
|
invitationEmail={email || undefined}
|
||||||
authMode={EAuthModes.SIGN_UP}
|
authMode={EAuthModes.SIGN_UP}
|
||||||
currentAuthStep={authStep}
|
currentAuthStep={authStep}
|
||||||
handleLoader={setIsLoading}
|
>
|
||||||
/>
|
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
|
||||||
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
|
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
|
||||||
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
|
)}
|
||||||
)}
|
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
|
||||||
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
|
{authStep === EAuthSteps.UNIQUE_CODE && (
|
||||||
{authStep === EAuthSteps.UNIQUE_CODE && (
|
<AuthUniqueCodeForm
|
||||||
<AuthUniqueCodeForm
|
email={email}
|
||||||
email={email}
|
handleEmailClear={() => {
|
||||||
handleEmailClear={() => {
|
setEmail("");
|
||||||
setEmail("");
|
setAuthStep(EAuthSteps.EMAIL);
|
||||||
setAuthStep(EAuthSteps.EMAIL);
|
}}
|
||||||
}}
|
submitButtonText="Continue"
|
||||||
submitButtonText="Continue"
|
mode={authMode}
|
||||||
mode={authMode}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
{authStep === EAuthSteps.PASSWORD && (
|
||||||
{authStep === EAuthSteps.PASSWORD && (
|
<AuthPasswordForm
|
||||||
<AuthPasswordForm
|
email={email}
|
||||||
email={email}
|
handleEmailClear={() => {
|
||||||
handleEmailClear={() => {
|
setEmail("");
|
||||||
setEmail("");
|
setAuthStep(EAuthSteps.EMAIL);
|
||||||
setAuthStep(EAuthSteps.EMAIL);
|
}}
|
||||||
}}
|
handleStepChange={(step) => setAuthStep(step)}
|
||||||
handleStepChange={(step) => setAuthStep(step)}
|
mode={authMode}
|
||||||
mode={authMode}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
{isOAuthEnabled && <OAuthOptions />}
|
||||||
{isOAuthEnabled && <OAuthOptions />}
|
<TermsAndConditions isSignUp={authMode === EAuthModes.SIGN_UP} />
|
||||||
<TermsAndConditions isSignUp={authMode === EAuthModes.SIGN_UP} />
|
</AuthHeader>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { CircleCheck, XCircle } from "lucide-react";
|
import { CircleCheck, XCircle } from "lucide-react";
|
||||||
import { IEmailCheckData } from "@plane/types";
|
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
|
// helpers
|
||||||
import { EAuthModes } from "@/helpers/authentication.helper";
|
import { EAuthModes } from "@/helpers/authentication.helper";
|
||||||
import { API_BASE_URL } from "@/helpers/common.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 [uniqueCodeFormData, setUniqueCodeFormData] = useState<TUniqueCodeFormValues>({ ...defaultValues, email });
|
||||||
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
||||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
// const { captureEvent } = useEventTracker();
|
// const { captureEvent } = useEventTracker();
|
||||||
// timer
|
// timer
|
||||||
@ -88,12 +89,15 @@ export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0;
|
const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0;
|
||||||
|
const isButtonDisabled = isRequestingNewCode || !uniqueCodeFormData.code || isSubmitting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
|
className="mt-5 space-y-4"
|
||||||
method="POST"
|
method="POST"
|
||||||
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "magic-sign-in" : "magic-sign-up"}/`}
|
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} />
|
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@ -159,15 +163,14 @@ export const AuthUniqueCodeForm: React.FC<Props> = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||||
type="submit"
|
{isRequestingNewCode ? (
|
||||||
variant="primary"
|
"Sending code"
|
||||||
className="w-full"
|
) : isSubmitting ? (
|
||||||
size="lg"
|
<Spinner height="20px" width="20px" />
|
||||||
loading={isRequestingNewCode}
|
) : (
|
||||||
disabled={isRequestingNewCode || !uniqueCodeFormData.code}
|
submitButtonText
|
||||||
>
|
)}
|
||||||
{isRequestingNewCode ? "Sending code" : submitButtonText}
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</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 "./oauth-options";
|
||||||
export * from "./google-button";
|
export * from "./google-button";
|
||||||
export * from "./github-button";
|
export * from "./github-button";
|
||||||
|
@ -10,12 +10,12 @@ export const OAuthOptions: React.FC = observer(() => {
|
|||||||
|
|
||||||
return (
|
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" />
|
<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>
|
<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" />
|
<hr className="w-full border-onboarding-border-100" />
|
||||||
</div>
|
</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 && (
|
{instance?.config?.is_google_enabled && (
|
||||||
<div className="flex h-[42px] items-center !overflow-hidden">
|
<div className="flex h-[42px] items-center !overflow-hidden">
|
||||||
<GoogleOAuthButton text="SignIn with Google" />
|
<GoogleOAuthButton text="SignIn with Google" />
|
||||||
|
@ -26,7 +26,7 @@ const inputRules = {
|
|||||||
|
|
||||||
export const CustomThemeSelector: React.FC = observer(() => {
|
export const CustomThemeSelector: React.FC = observer(() => {
|
||||||
const {
|
const {
|
||||||
profile: { data: userProfile },
|
userProfile: { data: userProfile },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
|
||||||
const userTheme: any = userProfile?.theme;
|
const userTheme: any = userProfile?.theme;
|
||||||
|
@ -18,7 +18,6 @@ import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys";
|
|||||||
import { EXPORTERS_LIST } from "@/constants/workspace";
|
import { EXPORTERS_LIST } from "@/constants/workspace";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "@/hooks/store";
|
import { useUser } from "@/hooks/store";
|
||||||
import useUserAuth from "@/hooks/use-user-auth";
|
|
||||||
// services
|
// services
|
||||||
import { IntegrationService } from "@/services/integrations";
|
import { IntegrationService } from "@/services/integrations";
|
||||||
|
|
||||||
@ -33,9 +32,7 @@ const IntegrationGuide = observer(() => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, provider } = router.query;
|
const { workspaceSlug, provider } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { data: currentUser, isLoading: currentUserLoader, profile } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
// custom hooks
|
|
||||||
const {} = useUserAuth({ user: currentUser || null, userProfile: profile?.data, isLoading: currentUserLoader });
|
|
||||||
|
|
||||||
const { data: exporterServices } = useSWR(
|
const { data: exporterServices } = useSWR(
|
||||||
workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null,
|
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
|
// hooks
|
||||||
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
|
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const { currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
// states
|
// states
|
||||||
const [itemsToRender, setItemsToRender] = useState(5);
|
const [itemsToRender, setItemsToRender] = useState(5);
|
||||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
@ -1,34 +1,31 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// images
|
// 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 PlaneTakeOffImage from "@/public/plane-takeoff.png";
|
||||||
|
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||||
|
|
||||||
export const InstanceNotReady: FC = () => {
|
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`;
|
const planeGodModeUrl = `${process.env.NEXT_PUBLIC_GOD_MODE_URL}/god-mode/setup/?auth_enabled=0`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-screen max-h-max w-full overflow-hidden overflow-y-auto flex flex-col">
|
<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="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">
|
<div className="flex items-center gap-x-2 py-10">
|
||||||
<Image src={planeLogo} className="h-[24px] w-full" alt="Plane logo" />
|
<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>
|
||||||
</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="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="w-auto max-w-2xl relative space-y-8 py-10">
|
||||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
<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>
|
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
|
||||||
<Image src={PlaneTakeOffImage} alt="Plane Logo" />
|
<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
|
Get started by setting up your instance and workspace
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,7 +20,6 @@ import { IMPORTER_SERVICES_LIST } from "@/constants/fetch-keys";
|
|||||||
import { IMPORTERS_LIST } from "@/constants/workspace";
|
import { IMPORTERS_LIST } from "@/constants/workspace";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "@/hooks/store";
|
import { useUser } from "@/hooks/store";
|
||||||
import useUserAuth from "@/hooks/use-user-auth";
|
|
||||||
// services
|
// services
|
||||||
import { IntegrationService } from "@/services/integrations";
|
import { IntegrationService } from "@/services/integrations";
|
||||||
|
|
||||||
@ -36,9 +35,7 @@ const IntegrationGuide = observer(() => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, provider } = router.query;
|
const { workspaceSlug, provider } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { data: currentUser, isLoading: currentUserLoader, profile } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
// custom hooks
|
|
||||||
const {} = useUserAuth({ user: currentUser || null, userProfile: profile?.data, isLoading: currentUserLoader });
|
|
||||||
|
|
||||||
const { data: importerServices } = useSWR(
|
const { data: importerServices } = useSWR(
|
||||||
workspaceSlug ? IMPORTER_SERVICES_LIST(workspaceSlug as string) : null,
|
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);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const { currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
|
|
||||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ export const FilterCreatedBy: React.FC<Props> = observer((props: Props) => {
|
|||||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const { currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
|
|
||||||
const sortedOptions = useMemo(() => {
|
const sortedOptions = useMemo(() => {
|
||||||
const filteredOptions = (memberIds || []).filter((memberId) =>
|
const filteredOptions = (memberIds || []).filter((memberId) =>
|
||||||
|
@ -22,7 +22,7 @@ export const FilterMentions: React.FC<Props> = observer((props: Props) => {
|
|||||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const { currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
|
|
||||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ export const FilterLead: React.FC<Props> = observer((props: Props) => {
|
|||||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const { currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
|
|
||||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ export const FilterMembers: React.FC<Props> = observer((props: Props) => {
|
|||||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const { currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
|
|
||||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
// types
|
// types
|
||||||
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
|
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Button, CustomSelect, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { WORKSPACE_CREATED } from "@/constants/event-tracker";
|
import { WORKSPACE_CREATED } from "@/constants/event-tracker";
|
||||||
import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "@/constants/workspace";
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{!!invitedWorkspaces && (
|
{!!invitedWorkspaces && (
|
||||||
@ -256,8 +258,8 @@ export const CreateWorkspace: React.FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="primary" type="submit" size="lg" className="w-full" disabled={!isValid || invalidSlug}>
|
<Button variant="primary" type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
|
||||||
{isSubmitting ? "Creating..." : "Continue"}
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@ import useSWR from "swr";;
|
|||||||
// types
|
// types
|
||||||
import { IWorkspaceMemberInvitation } from "@plane/types";
|
import { IWorkspaceMemberInvitation } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Checkbox } from "@plane/ui";
|
import { Button, Checkbox, Spinner } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { MEMBER_ACCEPTED } from "@/constants/event-tracker";
|
import { MEMBER_ACCEPTED } from "@/constants/event-tracker";
|
||||||
import { USER_WORKSPACE_INVITATIONS } from "@/constants/fetch-keys";
|
import { USER_WORKSPACE_INVITATIONS } from "@/constants/fetch-keys";
|
||||||
@ -127,7 +127,7 @@ export const Invitations: React.FC<Props> = (props) => {
|
|||||||
onClick={submitInvitations}
|
onClick={submitInvitations}
|
||||||
disabled={isJoiningWorkspaces || !invitationsRespond.length}
|
disabled={isJoiningWorkspaces || !invitationsRespond.length}
|
||||||
>
|
>
|
||||||
Continue to workspace
|
{isJoiningWorkspaces ? <Spinner height="20px" width="20px" /> : "Continue to workspace"}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="mx-auto mt-4 flex items-center sm:w-96">
|
<div className="mx-auto mt-4 flex items-center sm:w-96">
|
||||||
<hr className="w-full border-onboarding-border-100" />
|
<hr className="w-full border-onboarding-border-100" />
|
||||||
|
@ -18,7 +18,7 @@ import { Listbox, Transition } from "@headlessui/react";
|
|||||||
// types
|
// types
|
||||||
import { IUser, IWorkspace } from "@plane/types";
|
import { IUser, IWorkspace } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { MEMBER_INVITED } from "@/constants/event-tracker";
|
import { MEMBER_INVITED } from "@/constants/event-tracker";
|
||||||
import { EUserWorkspaceRoles, ROLE, ROLE_DETAILS } from "@/constants/workspace";
|
import { EUserWorkspaceRoles, ROLE, ROLE_DETAILS } from "@/constants/workspace";
|
||||||
@ -420,10 +420,9 @@ export const InviteMembers: React.FC<Props> = (props) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
disabled={isInvitationDisabled || !isValid}
|
disabled={isInvitationDisabled || !isValid || isSubmitting}
|
||||||
loading={isSubmitting}
|
|
||||||
>
|
>
|
||||||
Continue
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="link-neutral" size="lg" className="w-full" onClick={nextStep}>
|
<Button variant="link-neutral" size="lg" className="w-full" onClick={nextStep}>
|
||||||
I’ll do it later
|
I’ll do it later
|
||||||
|
@ -7,7 +7,7 @@ import { Eye, EyeOff } from "lucide-react";
|
|||||||
// types
|
// types
|
||||||
import { IUser, TUserProfile, TOnboardingSteps } from "@plane/types";
|
import { IUser, TUserProfile, TOnboardingSteps } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { PasswordStrengthMeter } from "@/components/account";
|
import { PasswordStrengthMeter } from "@/components/account";
|
||||||
import { UserImageUploadModal } from "@/components/core";
|
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.
|
// 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(
|
const isButtonDisabled = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
!isSubmitting &&
|
||||||
isValid &&
|
isValid &&
|
||||||
(isPasswordAlreadySetup
|
(isPasswordAlreadySetup
|
||||||
? true
|
? true
|
||||||
@ -258,7 +259,7 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
|
|||||||
: true)
|
: true)
|
||||||
? false
|
? false
|
||||||
: true,
|
: true,
|
||||||
[isValid, isPasswordAlreadySetup, isSignUpUsingMagicCode, password, confirmPassword]
|
[isSubmitting, isValid, isPasswordAlreadySetup, isSignUpUsingMagicCode, password, confirmPassword]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isCurrentStepUserPersonalization = profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION;
|
const isCurrentStepUserPersonalization = profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION;
|
||||||
@ -541,15 +542,8 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button variant="primary" type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
|
||||||
variant="primary"
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||||
type="submit"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
disabled={isButtonDisabled}
|
|
||||||
loading={isSubmitting}
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,2 +1 @@
|
|||||||
export * from "./signup";
|
|
||||||
export * from "./workspace-dashboard";
|
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);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const { currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
|
|
||||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ export const PreferencesMobileHeader = () => {
|
|||||||
<Link
|
<Link
|
||||||
key={index}
|
key={index}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
onClick={() => console.log(router.asPath)}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex justify-around py-2 w-full",
|
"flex justify-around py-2 w-full",
|
||||||
router.asPath.includes(link.label.toLowerCase()) ? "border-b-2 border-custom-primary-100" : ""
|
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);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const { currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
|
|
||||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ export const FilterMembers: React.FC<Props> = observer((props: Props) => {
|
|||||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const { currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
|
|
||||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ import { useUser } from "@/hooks/store";
|
|||||||
export const WorkspaceActiveCyclesUpgrade = observer(() => {
|
export const WorkspaceActiveCyclesUpgrade = observer(() => {
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
profile: { data: userProfile },
|
userProfile: { data: userProfile },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
|
||||||
const isDarkMode = userProfile?.theme.theme === "dark";
|
const isDarkMode = userProfile?.theme.theme === "dark";
|
||||||
|
@ -27,8 +27,7 @@ export * from "./use-issue-detail";
|
|||||||
// project inbox
|
// project inbox
|
||||||
export * from "./use-project-inbox";
|
export * from "./use-project-inbox";
|
||||||
export * from "./use-inbox-issues";
|
export * from "./use-inbox-issues";
|
||||||
export * from "./use-user-profile";
|
export * from "./user";
|
||||||
export * from "./use-user";
|
|
||||||
export * from "./use-instance";
|
export * from "./use-instance";
|
||||||
export * from "./use-app-theme";
|
export * from "./use-app-theme";
|
||||||
export * from "./use-command-palette";
|
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 { useCallback } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// hooks
|
import { IUser, TUserProfile } from "@plane/types";
|
||||||
import useUserAuth from "@/hooks/use-user-auth";
|
|
||||||
// services
|
// services
|
||||||
import { NotificationService } from "@/services/notification.service";
|
import { NotificationService } from "@/services/notification.service";
|
||||||
// types
|
// types
|
||||||
import { IUser, TUserProfile } from "@plane/types";
|
|
||||||
|
|
||||||
const userNotificationServices = new NotificationService();
|
const userNotificationServices = new NotificationService();
|
||||||
|
|
||||||
@ -16,8 +14,6 @@ const useUserIssueNotificationSubscription = (
|
|||||||
projectId?: string | string[] | null,
|
projectId?: string | string[] | null,
|
||||||
issueId?: string | string[] | null
|
issueId?: string | string[] | null
|
||||||
) => {
|
) => {
|
||||||
const {} = useUserAuth({ user: user, userProfile: profile, isLoading: false });
|
|
||||||
|
|
||||||
const { data, error, mutate } = useSWR(
|
const { data, error, mutate } = useSWR(
|
||||||
workspaceSlug && projectId && issueId ? `SUBSCRIPTION_STATUE_${workspaceSlug}_${projectId}_${issueId}` : null,
|
workspaceSlug && projectId && issueId ? `SUBSCRIPTION_STATUE_${workspaceSlug}_${projectId}_${issueId}` : null,
|
||||||
workspaceSlug && projectId && issueId
|
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 "./workspace-wrapper";
|
||||||
export * from "./project-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";
|
import { FC, ReactNode } from "react";
|
||||||
// layout
|
// layout
|
||||||
import { CommandPalette } from "@/components/command-palette";
|
import { CommandPalette } from "@/components/command-palette";
|
||||||
import { UserAuthWrapper } from "@/layouts/auth-layout";
|
|
||||||
import { ProfileLayoutSidebar } from "@/layouts/settings-layout";
|
import { ProfileLayoutSidebar } from "@/layouts/settings-layout";
|
||||||
|
// wrappers
|
||||||
|
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||||
// components
|
// components
|
||||||
|
|
||||||
interface IProfileSettingsLayout {
|
interface IProfileSettingsLayout {
|
||||||
@ -16,7 +17,7 @@ export const ProfileSettingsLayout: FC<IProfileSettingsLayout> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
<UserAuthWrapper>
|
<AuthenticationWrapper>
|
||||||
<div className="relative flex h-screen w-full overflow-hidden">
|
<div className="relative flex h-screen w-full overflow-hidden">
|
||||||
<ProfileLayoutSidebar />
|
<ProfileLayoutSidebar />
|
||||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
<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>
|
<div className="h-full w-full overflow-hidden">{children}</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</UserAuthWrapper>
|
</AuthenticationWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,7 @@ import { Spinner } from "@plane/ui";
|
|||||||
// helpers
|
// helpers
|
||||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser, useWorkspace } from "@/hooks/store";
|
import { useUser, useUserProfile, useUserSettings, useWorkspace } from "@/hooks/store";
|
||||||
|
|
||||||
type TPageType = EPageTypes;
|
type TPageType = EPageTypes;
|
||||||
|
|
||||||
@ -26,32 +26,16 @@ export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props
|
|||||||
// props
|
// props
|
||||||
const { children, pageType = EPageTypes.AUTHENTICATED } = props;
|
const { children, pageType = EPageTypes.AUTHENTICATED } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const { isLoading: isUserLoading, data: currentUser, fetchCurrentUser } = useUser();
|
||||||
isLoading: isUserLoading,
|
const { data: currentUserProfile } = useUserProfile();
|
||||||
data: currentUser,
|
const { data: currentUserSettings } = useUserSettings();
|
||||||
currentUserSettings: { isLoading: currentUserSettingsLoader, data: currentUserSettings, fetchCurrentUserSettings },
|
const { workspaces } = useWorkspace();
|
||||||
profile: { isLoading: currentUserProfileLoader, data: currentUserProfile, fetchUserProfile },
|
|
||||||
fetchCurrentUser,
|
|
||||||
} = useUser();
|
|
||||||
const { loader: workspaceLoader, workspaces, fetchWorkspaces } = useWorkspace();
|
|
||||||
|
|
||||||
useSWR("USER_INFORMATION", async () => fetchCurrentUser(), {
|
const { isLoading: isUserSWRLoading } = useSWR("USER_INFORMATION", async () => await fetchCurrentUser(), {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
shouldRetryOnError: 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 => {
|
const getWorkspaceRedirectionUrl = (): string => {
|
||||||
let redirectionRoute = "/profile";
|
let redirectionRoute = "/profile";
|
||||||
|
|
||||||
@ -75,7 +59,7 @@ export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props
|
|||||||
return redirectionRoute;
|
return redirectionRoute;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isUserLoading || currentUserSettingsLoader || currentUserProfileLoader || workspaceLoader)
|
if (isUserSWRLoading || isUserLoading)
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-screen w-full items-center justify-center">
|
<div className="relative flex h-screen w-full items-center justify-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@ -87,13 +71,13 @@ export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props
|
|||||||
if (pageType === EPageTypes.NON_AUTHENTICATED) {
|
if (pageType === EPageTypes.NON_AUTHENTICATED) {
|
||||||
if (!currentUser?.id) return <>{children}</>;
|
if (!currentUser?.id) return <>{children}</>;
|
||||||
else {
|
else {
|
||||||
if (currentUserProfile?.is_onboarded) {
|
if (currentUserProfile?.id && currentUserProfile?.is_onboarded) {
|
||||||
const currentRedirectRoute = getWorkspaceRedirectionUrl();
|
const currentRedirectRoute = getWorkspaceRedirectionUrl();
|
||||||
router.push(currentRedirectRoute);
|
router.push(currentRedirectRoute);
|
||||||
return;
|
return <></>;
|
||||||
} else {
|
} else {
|
||||||
router.push("/onboarding");
|
router.push("/onboarding");
|
||||||
return;
|
return <></>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,26 +85,26 @@ export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props
|
|||||||
if (pageType === EPageTypes.ONBOARDING) {
|
if (pageType === EPageTypes.ONBOARDING) {
|
||||||
if (!currentUser?.id) {
|
if (!currentUser?.id) {
|
||||||
router.push("/accounts/sign-in");
|
router.push("/accounts/sign-in");
|
||||||
return;
|
return <></>;
|
||||||
} else {
|
} else {
|
||||||
if (currentUser && currentUserProfile?.is_onboarded) {
|
if (currentUser && currentUserProfile?.id && currentUserProfile?.is_onboarded) {
|
||||||
const currentRedirectRoute = getWorkspaceRedirectionUrl();
|
const currentRedirectRoute = getWorkspaceRedirectionUrl();
|
||||||
router.push(currentRedirectRoute);
|
router.push(currentRedirectRoute);
|
||||||
return;
|
return <></>;
|
||||||
} else return <>{children}</>;
|
} else return <>{children}</>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageType === EPageTypes.AUTHENTICATED) {
|
if (pageType === EPageTypes.AUTHENTICATED) {
|
||||||
if (currentUser?.id) {
|
if (currentUser?.id) {
|
||||||
if (currentUserProfile?.is_onboarded) return <>{children}</>;
|
if (currentUserProfile && currentUserProfile?.id && currentUserProfile?.is_onboarded) return <>{children}</>;
|
||||||
else {
|
else {
|
||||||
router.push(`/onboarding`);
|
router.push(`/onboarding`);
|
||||||
return;
|
return <></>;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
router.push("/accounts/sign-in");
|
router.push("/accounts/sign-in");
|
||||||
return;
|
return <></>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ const StoreWrapper: FC<TStoreWrapper> = observer((props) => {
|
|||||||
const { setQuery } = useAppRouter();
|
const { setQuery } = useAppRouter();
|
||||||
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
|
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
|
||||||
const {
|
const {
|
||||||
profile: { data: userProfile },
|
userProfile: { data: userProfile },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
// states
|
// states
|
||||||
const [dom, setDom] = useState<undefined | HTMLElement>();
|
const [dom, setDom] = useState<undefined | HTMLElement>();
|
||||||
|
@ -28,8 +28,8 @@ import { AuthenticationWrapper } from "@/lib/wrappers";
|
|||||||
// services
|
// services
|
||||||
import { AuthService } from "@/services/auth.service";
|
import { AuthService } from "@/services/auth.service";
|
||||||
// images
|
// images
|
||||||
import PlaneBackgroundPatternDark from "public/onboarding/background-pattern-dark.svg";
|
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
|
||||||
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
|
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
|
||||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||||
|
|
||||||
type TForgotPasswordFormValues = {
|
type TForgotPasswordFormValues = {
|
||||||
@ -96,25 +96,25 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative w-screen h-screen overflow-hidden">
|
||||||
<PageHead title="Forgot Password" />
|
<PageHead title="Forgot Password" />
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<Image
|
<Image
|
||||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||||
className="w-screen min-h-screen object-cover"
|
className="w-full h-full object-cover"
|
||||||
alt="Plane background pattern"
|
alt="Plane background pattern"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative z-10">
|
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
|
<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">
|
<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>
|
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center text-sm font-medium text-onboarding-text-300">
|
<div className="text-center text-sm font-medium text-onboarding-text-300">
|
||||||
New to Plane?{" "}
|
New to Plane?{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/accounts/sign-up"
|
href="/"
|
||||||
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
|
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
|
||||||
className="font-semibold text-custom-primary-100 hover:underline"
|
className="font-semibold text-custom-primary-100 hover:underline"
|
||||||
>
|
>
|
||||||
@ -122,66 +122,64 @@ const ForgotPasswordPage: NextPageWithLayout = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto h-full">
|
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
|
||||||
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
|
<div className="relative flex flex-col space-y-6">
|
||||||
<div className="mx-auto flex flex-col">
|
<div className="text-center space-y-1 py-4">
|
||||||
<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">
|
||||||
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
|
Reset your password
|
||||||
Reset your password
|
</h3>
|
||||||
</h3>
|
<p className="font-medium text-onboarding-text-400">
|
||||||
<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.
|
||||||
Enter your user account{"'"}s verified email address and we will send you a password reset link.
|
</p>
|
||||||
</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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,8 +22,8 @@ import { AuthenticationWrapper } from "@/lib/wrappers";
|
|||||||
// services
|
// services
|
||||||
import { AuthService } from "@/services/auth.service";
|
import { AuthService } from "@/services/auth.service";
|
||||||
// images
|
// images
|
||||||
import PlaneBackgroundPatternDark from "public/onboarding/background-pattern-dark.svg";
|
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
|
||||||
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
|
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
|
||||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||||
|
|
||||||
type TResetPasswordFormValues = {
|
type TResetPasswordFormValues = {
|
||||||
@ -74,120 +74,118 @@ const ResetPasswordPage: NextPageWithLayout = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative w-screen h-screen overflow-hidden">
|
||||||
<PageHead title="Reset Password" />
|
<PageHead title="Reset Password" />
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<Image
|
<Image
|
||||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||||
className="w-screen min-h-screen object-cover"
|
className="w-full h-full object-cover"
|
||||||
alt="Plane background pattern"
|
alt="Plane background pattern"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative z-10">
|
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
|
<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">
|
<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>
|
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto h-full">
|
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
|
||||||
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
|
<div className="relative flex flex-col space-y-6">
|
||||||
<div className="mx-auto flex flex-col">
|
<div className="text-center space-y-1 py-4">
|
||||||
<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">
|
||||||
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
|
Set new password
|
||||||
Set new password
|
</h3>
|
||||||
</h3>
|
<p className="font-medium text-onboarding-text-400">Secure your account with a strong password</p>
|
||||||
<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>
|
</div>
|
||||||
<form
|
<div className="space-y-1">
|
||||||
className="mx-auto mt-5 space-y-4 w-5/6 sm:w-96"
|
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
|
||||||
method="POST"
|
Password
|
||||||
action={`${API_BASE_URL}/auth/reset-password/${uidb64?.toString()}/${token?.toString()}/`}
|
</label>
|
||||||
>
|
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
||||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
<Input
|
||||||
<div className="space-y-1">
|
type={showPassword ? "text" : "password"}
|
||||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
|
name="password"
|
||||||
Email
|
value={resetFormData.password}
|
||||||
</label>
|
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||||
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
|
//hasError={Boolean(errors.password)}
|
||||||
<Input
|
placeholder="Enter password"
|
||||||
id="email"
|
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
|
||||||
name="email"
|
minLength={8}
|
||||||
type="email"
|
onFocus={() => setIsPasswordInputFocused(true)}
|
||||||
value={resetFormData.email}
|
onBlur={() => setIsPasswordInputFocused(false)}
|
||||||
//hasError={Boolean(errors.email)}
|
autoFocus
|
||||||
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"
|
{showPassword ? (
|
||||||
disabled
|
<EyeOff
|
||||||
|
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||||
|
onClick={() => setShowPassword(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
) : (
|
||||||
</div>
|
<Eye
|
||||||
<div className="space-y-1">
|
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
|
onClick={() => setShowPassword(true)}
|
||||||
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)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<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>
|
</div>
|
||||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
{isPasswordInputFocused && <PasswordStrengthMeter password={resetFormData.password} />}
|
||||||
Set password
|
</div>
|
||||||
</Button>
|
<div className="space-y-1">
|
||||||
</form>
|
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
|
||||||
</div>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// icons
|
// icons
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
@ -14,14 +15,16 @@ import { getPasswordStrength } from "@/helpers/password.helper";
|
|||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "@/hooks/store";
|
import { useUser } from "@/hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { UserAuthWrapper } from "@/layouts/auth-layout";
|
|
||||||
import DefaultLayout from "@/layouts/default-layout";
|
import DefaultLayout from "@/layouts/default-layout";
|
||||||
// lib
|
// lib
|
||||||
import { NextPageWithLayout } from "@/lib/types";
|
import { NextPageWithLayout } from "@/lib/types";
|
||||||
|
// wrappers
|
||||||
|
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||||
// services
|
// services
|
||||||
import { AuthService } from "@/services/auth.service";
|
import { AuthService } from "@/services/auth.service";
|
||||||
// images
|
// 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";
|
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||||
|
|
||||||
type TResetPasswordFormValues = {
|
type TResetPasswordFormValues = {
|
||||||
@ -50,6 +53,8 @@ const SetPasswordPage: NextPageWithLayout = observer(() => {
|
|||||||
});
|
});
|
||||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||||
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
||||||
|
// hooks
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
const { data: user } = useUser();
|
const { data: user } = useUser();
|
||||||
|
|
||||||
@ -90,112 +95,114 @@ const SetPasswordPage: NextPageWithLayout = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative w-screen h-screen overflow-hidden">
|
||||||
<PageHead title="Reset Password" />
|
<PageHead title="Reset Password" />
|
||||||
<div className="absolute inset-0 z-0">
|
<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>
|
||||||
<div className="relative z-10">
|
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
|
<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">
|
<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>
|
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto h-full">
|
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
|
||||||
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
|
<div className="relative flex flex-col space-y-6">
|
||||||
<div className="mx-auto flex flex-col">
|
<div className="text-center space-y-1 py-4">
|
||||||
<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">
|
||||||
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
|
Secure your account
|
||||||
Secure your account
|
</h3>
|
||||||
</h3>
|
<p className="font-medium text-onboarding-text-400">Setting password helps you login securely</p>
|
||||||
<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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -205,9 +212,9 @@ const SetPasswordPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
SetPasswordPage.getLayout = function getLayout(page: ReactElement) {
|
SetPasswordPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<UserAuthWrapper>
|
<AuthenticationWrapper>
|
||||||
<DefaultLayout>{page}</DefaultLayout>
|
<DefaultLayout>{page}</DefaultLayout>
|
||||||
</UserAuthWrapper>
|
</AuthenticationWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,8 +19,8 @@ import { NextPageWithLayout } from "@/lib/types";
|
|||||||
// wrappers
|
// wrappers
|
||||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||||
// assets
|
// assets
|
||||||
import PlaneBackgroundPatternDark from "public/onboarding/background-pattern-dark.svg";
|
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
|
||||||
import PlaneBackgroundPattern from "public/onboarding/background-pattern.svg";
|
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
|
||||||
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
|
||||||
|
|
||||||
export type AuthType = "sign-in" | "sign-up";
|
export type AuthType = "sign-in" | "sign-up";
|
||||||
@ -32,19 +32,19 @@ const SignInPage: NextPageWithLayout = observer(() => {
|
|||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative w-screen h-screen overflow-hidden">
|
||||||
<PageHead title="Sign In" />
|
<PageHead title="Sign In" />
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<Image
|
<Image
|
||||||
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
|
||||||
className="w-screen min-h-screen object-cover"
|
className="w-full h-full object-cover"
|
||||||
alt="Plane background pattern"
|
alt="Plane background pattern"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative z-10">
|
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||||
<div className="flex items-center justify-between px-8 pb-4 sm:px-16 sm:py-5 lg:px-28">
|
<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">
|
<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>
|
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center text-sm font-medium text-onboarding-text-300">
|
<div className="text-center text-sm font-medium text-onboarding-text-300">
|
||||||
@ -58,10 +58,8 @@ const SignInPage: NextPageWithLayout = observer(() => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto h-full">
|
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
|
||||||
<div className="h-full overflow-auto px-7 pb-56 pt-4 sm:px-0">
|
<SignInAuthRoot />
|
||||||
<SignInAuthRoot />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,11 +10,12 @@ import { PageHead } from "@/components/core";
|
|||||||
import { CreateWorkspaceForm } from "@/components/workspace";
|
import { CreateWorkspaceForm } from "@/components/workspace";
|
||||||
import { useUser } from "@/hooks/store";
|
import { useUser } from "@/hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { UserAuthWrapper } from "@/layouts/auth-layout";
|
|
||||||
import DefaultLayout from "@/layouts/default-layout";
|
import DefaultLayout from "@/layouts/default-layout";
|
||||||
// components
|
// components
|
||||||
// images
|
// images
|
||||||
import { NextPageWithLayout } from "@/lib/types";
|
import { NextPageWithLayout } from "@/lib/types";
|
||||||
|
// wrappers
|
||||||
|
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||||
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
|
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
|
||||||
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
|
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
|
||||||
// types
|
// types
|
||||||
@ -79,9 +80,9 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
CreateWorkspacePage.getLayout = function getLayout(page: ReactElement) {
|
CreateWorkspacePage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<UserAuthWrapper>
|
<AuthenticationWrapper>
|
||||||
<DefaultLayout>{page} </DefaultLayout>
|
<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
|
// 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
|
// helpers
|
||||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||||
|
// hooks
|
||||||
|
import { useEventTracker } from "@/hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "@/layouts/default-layout";
|
import DefaultLayout from "@/layouts/default-layout";
|
||||||
// type
|
// types
|
||||||
import { NextPageWithLayout } from "@/lib/types";
|
import { NextPageWithLayout } from "@/lib/types";
|
||||||
// wrappers
|
// wrappers
|
||||||
import { AuthenticationWrapper } from "@/lib/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) {
|
HomePage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -22,10 +22,11 @@ import { ROLE } from "@/constants/workspace";
|
|||||||
import { truncateText } from "@/helpers/string.helper";
|
import { truncateText } from "@/helpers/string.helper";
|
||||||
import { getUserRole } from "@/helpers/user.helper";
|
import { getUserRole } from "@/helpers/user.helper";
|
||||||
import { useEventTracker, useUser, useWorkspace } from "@/hooks/store";
|
import { useEventTracker, useUser, useWorkspace } from "@/hooks/store";
|
||||||
import { UserAuthWrapper } from "@/layouts/auth-layout";
|
|
||||||
import DefaultLayout from "@/layouts/default-layout";
|
import DefaultLayout from "@/layouts/default-layout";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "@/lib/types";
|
import { NextPageWithLayout } from "@/lib/types";
|
||||||
|
// wrappers
|
||||||
|
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||||
// services
|
// services
|
||||||
import { UserService } from "@/services/user.service";
|
import { UserService } from "@/services/user.service";
|
||||||
import { WorkspaceService } from "@/services/workspace.service";
|
import { WorkspaceService } from "@/services/workspace.service";
|
||||||
@ -238,9 +239,9 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
UserInvitationsPage.getLayout = function getLayout(page: ReactElement) {
|
UserInvitationsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<UserAuthWrapper>
|
<AuthenticationWrapper>
|
||||||
<DefaultLayout>{page}</DefaultLayout>
|
<DefaultLayout>{page}</DefaultLayout>
|
||||||
</UserAuthWrapper>
|
</AuthenticationWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,14 +12,16 @@ import { InviteMembers, CreateOrJoinWorkspaces, ProfileSetup } from "@/component
|
|||||||
// constants
|
// constants
|
||||||
import { USER_ONBOARDING_COMPLETED } from "@/constants/event-tracker";
|
import { USER_ONBOARDING_COMPLETED } from "@/constants/event-tracker";
|
||||||
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
|
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
|
||||||
|
// helpers
|
||||||
|
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser, useWorkspace, useUserProfile, useEventTracker } from "@/hooks/store";
|
import { useUser, useWorkspace, useUserProfile, useEventTracker } from "@/hooks/store";
|
||||||
import useUserAuth from "@/hooks/use-user-auth";
|
|
||||||
// layouts
|
// layouts
|
||||||
import { UserAuthWrapper } from "@/layouts/auth-layout";
|
|
||||||
import DefaultLayout from "@/layouts/default-layout";
|
import DefaultLayout from "@/layouts/default-layout";
|
||||||
// lib types
|
// lib types
|
||||||
import { NextPageWithLayout } from "@/lib/types";
|
import { NextPageWithLayout } from "@/lib/types";
|
||||||
|
// wrappers
|
||||||
|
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||||
// services
|
// services
|
||||||
import { WorkspaceService } from "@/services/workspace.service";
|
import { WorkspaceService } from "@/services/workspace.service";
|
||||||
|
|
||||||
@ -39,16 +41,10 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
const { data: user, isLoading: currentUserLoader, updateCurrentUser } = useUser();
|
const { data: user, updateCurrentUser } = useUser();
|
||||||
const { data: profile, updateUserOnBoard, updateUserProfile } = useUserProfile();
|
const { data: profile, updateUserOnBoard, updateUserProfile } = useUserProfile();
|
||||||
const { workspaces, fetchWorkspaces } = useWorkspace();
|
const { workspaces, fetchWorkspaces } = useWorkspace();
|
||||||
// custom hooks
|
|
||||||
const {} = useUserAuth({
|
|
||||||
routeAuth: "onboarding",
|
|
||||||
user: user || null,
|
|
||||||
userProfile: profile,
|
|
||||||
isLoading: currentUserLoader,
|
|
||||||
});
|
|
||||||
// computed values
|
// computed values
|
||||||
const workspacesList = Object.values(workspaces ?? {});
|
const workspacesList = Object.values(workspaces ?? {});
|
||||||
// fetching workspaces list
|
// fetching workspaces list
|
||||||
@ -145,7 +141,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
handleStepChange();
|
handleStepChange();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [user, step, updateCurrentUser, workspacesList]);
|
}, [user, step, profile.onboarding_step, updateCurrentUser, workspacesList]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -190,9 +186,9 @@ const OnboardingPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
OnboardingPage.getLayout = function getLayout(page: ReactElement) {
|
OnboardingPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<UserAuthWrapper>
|
<AuthenticationWrapper pageType={EPageTypes.ONBOARDING}>
|
||||||
<DefaultLayout>{page}</DefaultLayout>
|
<DefaultLayout>{page}</DefaultLayout>
|
||||||
</UserAuthWrapper>
|
</AuthenticationWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|