[WEB-4488] feat: brand revamp (#7544)
* chore: empty state asset and theme improvement (#7542) * chore: empty state asset and theme improvement * chore: upgrade modal improvement and code refactor * feat: onboarding revamp and theme changes (#7541) * refactor: consolidate password strength indicator into shared UI package * chore: remove old password strength meter implementations * chore: update package dependencies for password strength refactor * chore: code refactor * chore: brand logo added * chore: terms and conditions refactor * chore: auth form refactor * chore: oauth enhancements and refactor * chore: plane new logos added * chore: auth input form field added to ui package * chore: password input component added * chore: web auth refactor * chore: update brand colors and remove onboarding-specific styles * chore: clean up unused assets * chore: profile menu text overflow * chore: theme related changes * chore: logo spinner updated * chore: onboarding constant and types updated * chore: theme changes and code refactor * feat: onboarding flow revamp * fix: build error and code refactoring * chore: code refactor * fix: build error * chore: consent option added to onboarding and code refactor * fix: build fix * chore: code refactor * chore: auth screen revamp and code refactor * chore: onboarding enhancements * chore: code refactor * chore: onboarding logic improvement * chore: code refactor * fix: onboarding pre release improvements * chore: color token updated * chore: color token updated * chore: auth screen line height and size improvements * chore: input height updated * chore: n-progress theme updated * chore: theme and logo enhancements * chore: space auth and code refactor * chore: update new brand empty states (#7543) * [WEB-4585]chore: branding updates (#7540) * chore: updated logo, og image, and loaders * chore: updated branding colors * chore: tour modal logo * chore: updated logo spinner size * chore: updated email templates logos and colors * chore: code refactor * fix: removed conditional hook render * fix: space app loader --------- Co-authored-by: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Co-authored-by: vamsikrishnamathala <matalav55@gmail.com>
This commit is contained in:
parent
6450793d72
commit
51e146f8ca
345 changed files with 5158 additions and 2515 deletions
77
packages/ui/src/auth-form/auth-confirm-password-input.tsx
Normal file
77
packages/ui/src/auth-form/auth-confirm-password-input.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import React, { useState } from "react";
|
||||
import { cn } from "@plane/utils";
|
||||
import { AuthInput } from "./auth-input";
|
||||
|
||||
export interface AuthConfirmPasswordInputProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "autoComplete"> {
|
||||
password: string;
|
||||
label?: string;
|
||||
error?: string;
|
||||
showPasswordToggle?: boolean;
|
||||
containerClassName?: string;
|
||||
labelClassName?: string;
|
||||
errorClassName?: string;
|
||||
autoComplete?: "on" | "off";
|
||||
onPasswordMatchChange?: (matches: boolean) => void;
|
||||
}
|
||||
|
||||
export const AuthConfirmPasswordInput: React.FC<AuthConfirmPasswordInputProps> = ({
|
||||
password,
|
||||
label = "Confirm Password",
|
||||
error,
|
||||
showPasswordToggle = true,
|
||||
containerClassName = "",
|
||||
errorClassName = "",
|
||||
className = "",
|
||||
value = "",
|
||||
onChange,
|
||||
onPasswordMatchChange,
|
||||
...props
|
||||
}) => {
|
||||
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="on"
|
||||
/>
|
||||
{confirmPassword && passwordsMatch && <p className="text-sm text-green-500">Passwords match</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
41
packages/ui/src/auth-form/auth-forgot-password.tsx
Normal file
41
packages/ui/src/auth-form/auth-forgot-password.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import React from "react";
|
||||
import { cn } from "../../helpers";
|
||||
|
||||
export interface AuthForgotPasswordProps {
|
||||
onForgotPassword?: () => void;
|
||||
className?: string;
|
||||
text?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const AuthForgotPassword: React.FC<AuthForgotPasswordProps> = ({
|
||||
onForgotPassword,
|
||||
className = "",
|
||||
text = "Forgot your password?",
|
||||
disabled = false,
|
||||
}) => {
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (!disabled && onForgotPassword) {
|
||||
onForgotPassword();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"text-sm text-custom-primary-100 hover:text-custom-primary-200 transition-colors duration-200",
|
||||
{
|
||||
"opacity-50 cursor-not-allowed": disabled,
|
||||
"cursor-pointer": !disabled,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
207
packages/ui/src/auth-form/auth-form.tsx
Normal file
207
packages/ui/src/auth-form/auth-form.tsx
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import React, { useState, useMemo } from "react";
|
||||
import { E_PASSWORD_STRENGTH } from "@plane/constants";
|
||||
import { cn } from "../../helpers";
|
||||
import { Button } from "../button/button";
|
||||
import { Spinner } from "../spinners/circular-spinner";
|
||||
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 const AuthForm: React.FC<AuthFormProps> = ({
|
||||
mode,
|
||||
initialData = {},
|
||||
onSubmit,
|
||||
onForgotPassword,
|
||||
onModeChange,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
className = "",
|
||||
showForgotPassword = true,
|
||||
showPasswordStrength = true,
|
||||
emailError,
|
||||
passwordError,
|
||||
confirmPasswordError,
|
||||
submitButtonText,
|
||||
alternateModeText,
|
||||
alternateModeButtonText,
|
||||
}) => {
|
||||
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-sm text-custom-text-300">{getAlternateModeText()}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleModeChange}
|
||||
className="ml-1 text-sm text-custom-primary-100 hover:text-custom-primary-200 transition-colors duration-200"
|
||||
disabled={disabled}
|
||||
>
|
||||
{getAlternateModeButtonText()}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
66
packages/ui/src/auth-form/auth-input.tsx
Normal file
66
packages/ui/src/auth-form/auth-input.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { Eye, EyeOff } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { cn } from "../../helpers";
|
||||
import { Input } from "../form-fields/input";
|
||||
|
||||
export interface AuthInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "autoComplete"> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
showPasswordToggle?: boolean;
|
||||
errorClassName?: string;
|
||||
autoComplete?: "on" | "off";
|
||||
}
|
||||
|
||||
const baseContainerClassName = "flex flex-col gap-1.5";
|
||||
|
||||
export const AuthInput: React.FC<AuthInputProps> = ({
|
||||
label,
|
||||
error,
|
||||
showPasswordToggle = false,
|
||||
errorClassName = "",
|
||||
className = "",
|
||||
type = "text",
|
||||
...props
|
||||
}) => {
|
||||
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-sm font-semibold text-custom-text-300")}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div
|
||||
className={cn("relative flex items-center rounded-md border border-custom-border-300 py-2 px-3 transition-all")}
|
||||
>
|
||||
<Input
|
||||
{...props}
|
||||
type={inputType}
|
||||
className={cn(
|
||||
"rounded-md disable-autofill-style h-6 w-full placeholder:text-base placeholder:text-custom-text-400 p-0 border-none",
|
||||
{
|
||||
"border-red-500": error,
|
||||
},
|
||||
className
|
||||
)}
|
||||
/>
|
||||
{showPasswordToggle && isPasswordType && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <p className={cn("text-sm text-red-500", errorClassName)}>{error}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
77
packages/ui/src/auth-form/auth-password-input.tsx
Normal file
77
packages/ui/src/auth-form/auth-password-input.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import React, { useState } from "react";
|
||||
import { 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 interface AuthPasswordInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "autoComplete"> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
showPasswordStrength?: boolean;
|
||||
showPasswordToggle?: boolean;
|
||||
containerClassName?: string;
|
||||
errorClassName?: string;
|
||||
autoComplete?: "on" | "off";
|
||||
onPasswordChange?: (password: string) => void;
|
||||
onPasswordStrengthChange?: (strength: E_PASSWORD_STRENGTH) => void;
|
||||
}
|
||||
|
||||
export const AuthPasswordInput: React.FC<AuthPasswordInputProps> = ({
|
||||
label = "Password",
|
||||
error,
|
||||
showPasswordStrength = true,
|
||||
showPasswordToggle = true,
|
||||
containerClassName = "",
|
||||
errorClassName = "",
|
||||
className = "",
|
||||
value = "",
|
||||
onChange,
|
||||
onPasswordChange,
|
||||
onPasswordStrengthChange,
|
||||
...props
|
||||
}) => {
|
||||
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="on"
|
||||
/>
|
||||
{showPasswordStrength && value && isFocused && (
|
||||
<PasswordStrengthIndicator password={value as string} showCriteria />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
11
packages/ui/src/auth-form/index.ts
Normal file
11
packages/ui/src/auth-form/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
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 { AuthInputProps } from "./auth-input";
|
||||
export type { AuthPasswordInputProps } from "./auth-password-input";
|
||||
export type { AuthConfirmPasswordInputProps } from "./auth-confirm-password-input";
|
||||
export type { AuthForgotPasswordProps } from "./auth-forgot-password";
|
||||
Loading…
Add table
Add a link
Reference in a new issue