base
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { cn } from "@plane/utils";
|
||||
import { AuthInput } from "./auth-input";
|
||||
|
||||
export type TAuthConfirmPasswordInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
password: string;
|
||||
label?: string;
|
||||
error?: string;
|
||||
showPasswordToggle?: boolean;
|
||||
containerClassName?: string;
|
||||
labelClassName?: string;
|
||||
errorClassName?: string;
|
||||
onPasswordMatchChange?: (matches: boolean) => void;
|
||||
};
|
||||
|
||||
export function AuthConfirmPasswordInput({
|
||||
password,
|
||||
label = "Confirm Password",
|
||||
error,
|
||||
showPasswordToggle = true,
|
||||
containerClassName = "",
|
||||
errorClassName = "",
|
||||
className = "",
|
||||
value = "",
|
||||
onChange,
|
||||
onPasswordMatchChange,
|
||||
...props
|
||||
}: TAuthConfirmPasswordInputProps) {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const confirmPassword = value as string;
|
||||
const passwordsMatch = password === confirmPassword && password.length > 0;
|
||||
const showMatchError =
|
||||
confirmPassword.length > 0 && !passwordsMatch && (!isFocused || confirmPassword.length >= password.length);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newConfirmPassword = e.target.value;
|
||||
onChange?.(e);
|
||||
onPasswordMatchChange?.(password === newConfirmPassword && password.length > 0);
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsFocused(true);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsFocused(false);
|
||||
};
|
||||
|
||||
const getError = () => {
|
||||
if (error) return error;
|
||||
if (showMatchError) return "Passwords don't match";
|
||||
return "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-2", containerClassName)}>
|
||||
<AuthInput
|
||||
{...props}
|
||||
type="password"
|
||||
label={label}
|
||||
error={getError()}
|
||||
showPasswordToggle={showPasswordToggle}
|
||||
errorClassName={errorClassName}
|
||||
className={className}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{confirmPassword && passwordsMatch && <p className="text-13 text-success-primary">Passwords match</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { cn } from "../utils";
|
||||
|
||||
export interface AuthForgotPasswordProps {
|
||||
onForgotPassword?: () => void;
|
||||
className?: string;
|
||||
text?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function AuthForgotPassword({
|
||||
onForgotPassword,
|
||||
className = "",
|
||||
text = "Forgot your password?",
|
||||
disabled = false,
|
||||
}: AuthForgotPasswordProps) {
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (!disabled && onForgotPassword) {
|
||||
onForgotPassword();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"text-13 text-accent-primary transition-colors duration-200 hover:text-accent-secondary",
|
||||
{
|
||||
"cursor-not-allowed opacity-50": disabled,
|
||||
"cursor-pointer": !disabled,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { E_PASSWORD_STRENGTH } from "@plane/constants";
|
||||
import { Button } from "../button/button";
|
||||
import { Spinner } from "../spinners/circular-spinner";
|
||||
import { cn } from "../utils";
|
||||
import { AuthConfirmPasswordInput } from "./auth-confirm-password-input";
|
||||
import { AuthForgotPassword } from "./auth-forgot-password";
|
||||
import { AuthInput } from "./auth-input";
|
||||
import { AuthPasswordInput } from "./auth-password-input";
|
||||
|
||||
export type AuthMode = "sign-in" | "sign-up";
|
||||
|
||||
export interface AuthFormData {
|
||||
email: string;
|
||||
password: string;
|
||||
confirmPassword?: string;
|
||||
}
|
||||
|
||||
export interface AuthFormProps {
|
||||
mode: AuthMode;
|
||||
initialData?: Partial<AuthFormData>;
|
||||
onSubmit?: (data: AuthFormData) => void;
|
||||
onForgotPassword?: () => void;
|
||||
onModeChange?: (mode: AuthMode) => void;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
showForgotPassword?: boolean;
|
||||
showPasswordStrength?: boolean;
|
||||
emailError?: string;
|
||||
passwordError?: string;
|
||||
confirmPasswordError?: string;
|
||||
submitButtonText?: string;
|
||||
alternateModeText?: string;
|
||||
alternateModeButtonText?: string;
|
||||
}
|
||||
|
||||
export function AuthForm({
|
||||
mode,
|
||||
initialData = {},
|
||||
onSubmit,
|
||||
onForgotPassword,
|
||||
onModeChange,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
className = "",
|
||||
showForgotPassword = true,
|
||||
showPasswordStrength = true,
|
||||
emailError,
|
||||
passwordError,
|
||||
confirmPasswordError,
|
||||
submitButtonText,
|
||||
alternateModeText,
|
||||
alternateModeButtonText,
|
||||
}: AuthFormProps) {
|
||||
const [formData, setFormData] = useState<AuthFormData>({
|
||||
email: initialData.email || "",
|
||||
password: initialData.password || "",
|
||||
confirmPassword: initialData.confirmPassword || "",
|
||||
});
|
||||
|
||||
const [passwordStrength, setPasswordStrength] = useState<E_PASSWORD_STRENGTH>(E_PASSWORD_STRENGTH.EMPTY);
|
||||
const [_passwordsMatch, setPasswordsMatch] = useState(false);
|
||||
|
||||
const handleInputChange = (field: keyof AuthFormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: e.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handlePasswordChange = (password: string) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
password,
|
||||
}));
|
||||
};
|
||||
|
||||
const handlePasswordStrengthChange = (strength: E_PASSWORD_STRENGTH) => {
|
||||
setPasswordStrength(strength);
|
||||
};
|
||||
|
||||
const handleConfirmPasswordChange = (matches: boolean) => {
|
||||
setPasswordsMatch(matches);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (onSubmit && isFormValid) {
|
||||
onSubmit(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModeChange = () => {
|
||||
const newMode = mode === "sign-in" ? "sign-up" : "sign-in";
|
||||
onModeChange?.(newMode);
|
||||
};
|
||||
|
||||
const isFormValid = useMemo(() => {
|
||||
const hasEmail = formData.email.length > 0;
|
||||
const hasPassword = formData.password.length > 0;
|
||||
|
||||
if (mode === "sign-in") {
|
||||
return hasEmail && hasPassword && !loading && !disabled;
|
||||
} else {
|
||||
const isPasswordStrong = passwordStrength === E_PASSWORD_STRENGTH.STRENGTH_VALID;
|
||||
const passwordsMatch = formData.password === formData.confirmPassword && formData.password.length > 0;
|
||||
return hasEmail && hasPassword && isPasswordStrong && passwordsMatch && !loading && !disabled;
|
||||
}
|
||||
}, [mode, formData, passwordStrength, loading, disabled]);
|
||||
|
||||
const getSubmitButtonText = () => {
|
||||
if (submitButtonText) return submitButtonText;
|
||||
return mode === "sign-in" ? "Sign In" : "Create Account";
|
||||
};
|
||||
|
||||
const getAlternateModeText = () => {
|
||||
if (alternateModeText) return alternateModeText;
|
||||
return mode === "sign-in" ? "Don't have an account?" : "Already have an account?";
|
||||
};
|
||||
|
||||
const getAlternateModeButtonText = () => {
|
||||
if (alternateModeButtonText) return alternateModeButtonText;
|
||||
return mode === "sign-in" ? "Sign Up" : "Sign In";
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={cn("space-y-4", className)}>
|
||||
{/* Email Input */}
|
||||
<AuthInput
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange("email")}
|
||||
placeholder="name@company.com"
|
||||
error={emailError}
|
||||
disabled={disabled}
|
||||
// autoComplete="email"
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Password Input */}
|
||||
<AuthPasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
label={mode === "sign-in" ? "Password" : "Set a password"}
|
||||
value={formData.password}
|
||||
onChange={handleInputChange("password")}
|
||||
onPasswordChange={handlePasswordChange}
|
||||
onPasswordStrengthChange={handlePasswordStrengthChange}
|
||||
placeholder="Enter password"
|
||||
error={passwordError}
|
||||
showPasswordStrength={showPasswordStrength && mode === "sign-up"}
|
||||
disabled={disabled}
|
||||
// autoComplete={mode === "sign-in" ? "current-password" : "new-password"}
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Confirm Password Input (Sign Up Only) */}
|
||||
{mode === "sign-up" && (
|
||||
<AuthConfirmPasswordInput
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
password={formData.password}
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange("confirmPassword")}
|
||||
onPasswordMatchChange={handleConfirmPasswordChange}
|
||||
error={confirmPasswordError}
|
||||
disabled={disabled}
|
||||
// autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Forgot Password Link (Sign In Only) */}
|
||||
{mode === "sign-in" && showForgotPassword && (
|
||||
<div className="flex justify-end">
|
||||
<AuthForgotPassword onForgotPassword={onForgotPassword} disabled={disabled} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="space-y-2.5">
|
||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={!isFormValid} loading={loading}>
|
||||
{loading ? <Spinner height="20px" width="20px" /> : getSubmitButtonText()}
|
||||
</Button>
|
||||
|
||||
{/* Alternate Mode Button */}
|
||||
{onModeChange && (
|
||||
<div className="text-center">
|
||||
<span className="text-13 text-tertiary">{getAlternateModeText()}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleModeChange}
|
||||
className="ml-1 text-13 text-accent-primary transition-colors duration-200 hover:text-accent-secondary"
|
||||
disabled={disabled}
|
||||
>
|
||||
{getAlternateModeButtonText()}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { Input } from "../form-fields/input";
|
||||
import { cn } from "../utils";
|
||||
|
||||
export type TAuthInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
label?: string;
|
||||
error?: string;
|
||||
showPasswordToggle?: boolean;
|
||||
errorClassName?: string;
|
||||
};
|
||||
|
||||
const baseContainerClassName = "flex flex-col gap-1.5";
|
||||
|
||||
export function AuthInput({
|
||||
label,
|
||||
error,
|
||||
showPasswordToggle = false,
|
||||
errorClassName = "",
|
||||
className = "",
|
||||
type = "text",
|
||||
autoComplete = "off",
|
||||
...props
|
||||
}: TAuthInputProps) {
|
||||
const { id } = props;
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const isPasswordType = type === "password";
|
||||
|
||||
const inputType = isPasswordType && showPasswordToggle && showPassword ? "text" : type;
|
||||
|
||||
return (
|
||||
<div className={cn(baseContainerClassName)}>
|
||||
{label && (
|
||||
<label htmlFor={id} className={cn("text-13 font-semibold text-tertiary")}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className={cn("relative flex items-center rounded-md border border-strong px-3 py-2 transition-all")}>
|
||||
<Input
|
||||
{...props}
|
||||
type={inputType}
|
||||
autoComplete={autoComplete}
|
||||
className={cn(
|
||||
"h-6 w-full rounded-md border-none p-0 disable-autofill-style placeholder:text-14 placeholder:text-placeholder",
|
||||
{
|
||||
"border-danger-strong": error,
|
||||
},
|
||||
className
|
||||
)}
|
||||
/>
|
||||
{showPasswordToggle && isPasswordType && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 h-5 w-5 stroke-placeholder hover:cursor-pointer"
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <p className={cn("text-13 text-danger-primary", errorClassName)}>{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import type { E_PASSWORD_STRENGTH } from "@plane/constants";
|
||||
import { cn, getPasswordStrength } from "@plane/utils";
|
||||
import { PasswordStrengthIndicator } from "../form-fields/password/indicator";
|
||||
import { AuthInput } from "./auth-input";
|
||||
|
||||
export type TAuthPasswordInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
label?: string;
|
||||
error?: string;
|
||||
showPasswordStrength?: boolean;
|
||||
showPasswordToggle?: boolean;
|
||||
containerClassName?: string;
|
||||
errorClassName?: string;
|
||||
onPasswordChange?: (password: string) => void;
|
||||
onPasswordStrengthChange?: (strength: E_PASSWORD_STRENGTH) => void;
|
||||
};
|
||||
|
||||
export function AuthPasswordInput({
|
||||
label = "Password",
|
||||
error,
|
||||
showPasswordStrength = true,
|
||||
showPasswordToggle = true,
|
||||
containerClassName = "",
|
||||
errorClassName = "",
|
||||
className = "",
|
||||
value = "",
|
||||
onChange,
|
||||
onPasswordChange,
|
||||
onPasswordStrengthChange,
|
||||
...props
|
||||
}: TAuthPasswordInputProps) {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newPassword = e.target.value;
|
||||
onChange?.(e);
|
||||
onPasswordChange?.(newPassword);
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsFocused(true);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsFocused(false);
|
||||
};
|
||||
|
||||
const passwordStrength = getPasswordStrength(value as string);
|
||||
|
||||
// Notify parent of strength change
|
||||
React.useEffect(() => {
|
||||
onPasswordStrengthChange?.(passwordStrength);
|
||||
}, [passwordStrength, onPasswordStrengthChange]);
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-2", containerClassName)}>
|
||||
<AuthInput
|
||||
{...props}
|
||||
type="password"
|
||||
label={label}
|
||||
error={error}
|
||||
showPasswordToggle={showPasswordToggle}
|
||||
errorClassName={errorClassName}
|
||||
className={className}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{showPasswordStrength && value && isFocused && (
|
||||
<PasswordStrengthIndicator password={value as string} showCriteria />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export { AuthForm } from "./auth-form";
|
||||
export { AuthInput } from "./auth-input";
|
||||
export { AuthPasswordInput } from "./auth-password-input";
|
||||
export { AuthConfirmPasswordInput } from "./auth-confirm-password-input";
|
||||
export { AuthForgotPassword } from "./auth-forgot-password";
|
||||
|
||||
export type { AuthFormProps, AuthFormData, AuthMode } from "./auth-form";
|
||||
export type { TAuthInputProps } from "./auth-input";
|
||||
export type { TAuthPasswordInputProps } from "./auth-password-input";
|
||||
export type { TAuthConfirmPasswordInputProps } from "./auth-confirm-password-input";
|
||||
export type { AuthForgotPasswordProps } from "./auth-forgot-password";
|
||||
Reference in New Issue
Block a user