feat: Cập nhật giao diện các trang xác thực với nền vũ trụ và hiệu ứng kínhmorphism, cải thiện trải nghiệm người dùng và thêm chức năng hiển thị mật khẩu cho trường nhập liệu.

This commit is contained in:
Ho Ngoc Hai
2026-01-04 23:23:08 +07:00
parent 2e26f71e39
commit d678d3aa8d
4 changed files with 464 additions and 349 deletions

View File

@@ -107,34 +107,65 @@ export default function ForgotPasswordPage() {
};
return (
// EN: Centered forgot password form layout with dark mode background
// VI: Layout form quên mật khẩu được căn giữa với nền dark mode
<>
<AuthControls />
<div className="min-h-screen flex items-center justify-center bg-bg-primary px-4 py-12">
<Card className="w-full max-w-md" hover={false} bordered>
<CardHeader className="text-center">
<CardTitle className="text-2xl font-semibold">
{t('auth.forgotPassword.title')}
</CardTitle>
<CardDescription className="mt-2">
{isSuccess
? t('auth.forgotPassword.checkEmail')
: t('auth.forgotPassword.description')}
</CardDescription>
</CardHeader>
// EN: Centered forgot password form layout with cosmic background and glassmorphism
// VI: Layout form quên mật khẩu với nền vũ trụ và hiệu ứng kínhmorphism
<main
role="main"
aria-label={t('auth.forgotPassword.title')}
className="min-h-screen flex items-center justify-center relative overflow-hidden bg-black py-12 px-4 sm:px-6 lg:px-8"
>
{/* EN: Cosmic Background Elements */}
{/* VI: Các thành phần nền vũ trụ */}
<div className="absolute inset-0 z-0 pointer-events-none">
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-accent-primary/5 blur-[120px] rounded-full animate-float opacity-50" />
<div
className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-accent-primary/5 blur-[120px] rounded-full animate-float opacity-50"
style={{ animationDelay: '-2s' }}
/>
</div>
<AuthControls />
<div className="w-full max-w-md space-y-8 relative z-10 glass-appear">
<div className="text-center space-y-4">
<div className="flex justify-center">
<div className="p-3 rounded-2xl bg-white/5 border border-white/10 shadow-glass-sm animate-float">
<svg
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-white"
>
<path
d="M12 2L14.4 9.6H22L15.8 14.2L18.2 21.8L12 17.2L5.8 21.8L8.2 14.2L2 9.6H9.6L12 2Z"
fill="currentColor"
/>
</svg>
</div>
</div>
<h1 className="text-4xl font-bold tracking-tight text-white mb-2">
{t('auth.forgotPassword.title')}
</h1>
<p className="text-text-secondary">
{isSuccess
? t('auth.forgotPassword.checkEmail')
: t('auth.forgotPassword.description')}
</p>
</div>
<div className="glass-card p-8 shadow-glass-xl border-glass-hover/20">
{isSuccess ? (
// EN: Success state - show confirmation message
// VI: Trạng thái thành công - hiển thị thông báo xác nhận
<CardContent className="space-y-4">
{/* EN: Success icon and message / VI: Icon và thông báo thành công */}
<div className="space-y-6 animate-in fade-in zoom-in duration-500">
<div
className="p-4 rounded-lg bg-bg-tertiary border border-accent-success text-accent-success flex items-center gap-3"
className="p-4 rounded-xl bg-accent-success/10 border border-accent-success/20 text-accent-success flex items-center gap-3"
role="alert"
>
<svg
className="h-5 w-5 flex-shrink-0"
className="h-6 w-6 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -147,124 +178,126 @@ export default function ForgotPasswordPage() {
d="M5 13l4 4L19 7"
/>
</svg>
<span className="text-sm font-medium">
<span className="text-sm font-semibold">
{t('auth.forgotPassword.resetLinkSent')}
</span>
</div>
{/* EN: Detailed success message / VI: Thông báo thành công chi tiết */}
<div className="space-y-3 text-sm text-text-secondary">
<div className="space-y-4 text-sm text-text-secondary leading-relaxed">
<p>
{t('auth.forgotPassword.resetLinkSentDetail', { email: submittedEmail })}
{t('auth.forgotPassword.resetLinkSentDetail', {
email: submittedEmail,
})}
</p>
<div className="pt-2 border-t border-border-primary">
<div className="pt-4 border-t border-glass-subtle">
<p className="text-text-tertiary">
{t('auth.forgotPassword.checkInbox')}
</p>
</div>
</div>
{/* EN: Additional action buttons / VI: Các nút hành động bổ sung */}
<div className="pt-2 space-y-2">
<Button
variant="ghost"
size="sm"
className="w-full"
onClick={() => {
setIsSuccess(false);
setSubmittedEmail('');
}}
>
{t('auth.forgotPassword.sendToAnotherEmail')}
</Button>
</div>
</CardContent>
<Button
variant="brand"
size="lg"
className="w-full mt-4"
onPress={() => (window.location.href = '/login')}
>
{t('auth.forgotPassword.backToLogin')}
</Button>
<button
type="button"
className="w-full text-sm text-text-tertiary hover:text-white transition-colors"
onClick={() => {
setIsSuccess(false);
setSubmittedEmail('');
}}
>
{t('auth.forgotPassword.sendToAnotherEmail')}
</button>
</div>
) : (
// EN: Form state - email input and submit
// VI: Trạng thái form - input email và submit
<form onSubmit={handleSubmit(onSubmit)}>
<CardContent className="space-y-4">
{/* EN: API Error message display / VI: Hiển thị thông báo lỗi API */}
{apiError && (
<div
className="p-3 rounded-md bg-bg-tertiary border border-accent-error text-accent-error text-sm flex items-center gap-2"
role="alert"
>
<svg
className="h-4 w-4 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{apiError}</span>
</div>
)}
{/* EN: Email input field / VI: Trường nhập email */}
<Input
type="email"
label={t('auth.forgotPassword.email')}
placeholder="you@example.com"
isInvalid={!!errors.email}
errorMessage={errors.email?.message}
name="email"
value={watch('email')}
onChange={(value) => setValue('email', value)}
onBlur={register('email').onBlur}
autoComplete="email"
autoFocus
aria-required="true"
/>
</CardContent>
<CardFooter className="flex flex-col gap-4">
{/* EN: Submit button with loading state / VI: Nút submit với trạng thái loading */}
<Button
type="submit"
variant="primary"
size="lg"
className="w-full"
isLoading={isSubmitting}
isDisabled={isSubmitting}
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
{/* EN: API Error message display / VI: Hiển thị thông báo lỗi API */}
{apiError && (
<div
className="p-3 rounded-lg bg-accent-error/10 border border-accent-error/20 text-accent-error text-sm flex items-center gap-2 animate-in fade-in duration-quick"
role="alert"
>
{isSubmitting
? t('auth.forgotPassword.sending')
: t('auth.forgotPassword.sendResetLink')}
</Button>
</CardFooter>
<svg
className="h-4 w-4 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{apiError}</span>
</div>
)}
<Input
type="email"
label={t('auth.forgotPassword.email')}
placeholder="you@example.com"
isInvalid={!!errors.email}
errorMessage={errors.email?.message}
{...register('email')}
onChange={(e) => {
register('email').onChange(e);
setValue('email', e.target.value);
}}
autoComplete="email"
autoFocus
aria-required="true"
/>
<Button
type="submit"
variant="brand"
size="lg"
className="w-full mt-4"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isSubmitting
? t('auth.forgotPassword.sending')
: t('auth.forgotPassword.sendResetLink')}
</Button>
<div className="text-center pt-4 border-t border-glass-subtle">
<Link
href="/login"
className="text-sm font-medium text-text-secondary hover:text-white transition-colors inline-flex items-center gap-2 group"
>
<svg
className="h-4 w-4 transform group-hover:-translate-x-1 transition-transform"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
{t('auth.forgotPassword.backToLogin')}
</Link>
</div>
</form>
)}
<CardFooter className="flex flex-col gap-2 pt-4">
{/* EN: Back to login link / VI: Link quay lại đăng nhập */}
<Link
href="/login"
className="text-sm text-accent-primary hover:brightness-110 transition-colors focus:outline-none focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary rounded"
>
{t('auth.forgotPassword.backToLogin')}
</Link>
{/* EN: Sign up link / VI: Link đăng ký */}
<p className="text-sm text-center text-text-tertiary">
{t('auth.forgotPassword.noAccount')}{' '}
<Link
href="/register"
className="text-accent-primary hover:brightness-110 font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary rounded"
>
{t('auth.forgotPassword.signUp')}
</Link>
</p>
</CardFooter>
</Card>
</div>
</div>
</>
</main>
);
}

View File

@@ -108,47 +108,77 @@ export default function LoginPage() {
};
return (
// EN: Centered login form layout with dark mode background
// VI: Layout form đăng nhập được căn giữa với nền dark mode
<main role="main" aria-label={t('auth.login.pageLabel')}>
// EN: Centered login form layout with cosmic background and glassmorphism
// VI: Layout form đăng nhập với nền vũ trụ và hiệu ứng kínhmorphism
<main
role="main"
aria-label={t('auth.login.pageLabel')}
className="min-h-screen flex items-center justify-center relative overflow-hidden bg-black py-12 px-4 sm:px-6 lg:px-8"
>
{/* EN: Cosmic Background Elements - X.ai style */}
{/* VI: Các thành phần nền vũ trụ - phong cách X.ai */}
<div className="absolute inset-0 z-0 pointer-events-none">
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-accent-primary/5 blur-[120px] rounded-full animate-float opacity-50" />
<div
className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-accent-primary/5 blur-[120px] rounded-full animate-float opacity-50"
style={{ animationDelay: '-2s' }}
/>
</div>
<AuthControls />
<div className="min-h-screen flex items-center justify-center bg-bg-primary px-4 py-12">
<Card className="w-full max-w-md" hover={false} bordered>
<CardHeader className="text-center">
<CardTitle className="text-2xl font-semibold">
{t('auth.login.title')}
</CardTitle>
<CardDescription className="mt-2">
{t('auth.login.description')}
</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit(onSubmit)}>
<CardContent className="space-y-4">
{/* EN: API Error message display / VI: Hiển thị thông báo lỗi API */}
{apiError && (
<div
className="p-3 rounded-md bg-bg-tertiary border border-accent-error text-accent-error text-sm flex items-center gap-2"
role="alert"
<div className="w-full max-w-md space-y-8 relative z-10 glass-appear">
<div className="text-center space-y-4">
<div className="flex justify-center">
<div className="p-3 rounded-2xl bg-white/5 border border-white/10 shadow-glass-sm animate-float">
<svg
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-white"
>
<path
d="M12 2L14.4 9.6H22L15.8 14.2L18.2 21.8L12 17.2L5.8 21.8L8.2 14.2L2 9.6H9.6L12 2Z"
fill="currentColor"
/>
</svg>
</div>
</div>
<h1 className="text-4xl font-bold tracking-tight text-white mb-2">
{t('auth.login.title')}
</h1>
<p className="text-text-secondary">{t('auth.login.description')}</p>
</div>
<div className="glass-card p-8 shadow-glass-xl border-glass-hover/20">
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
{/* EN: API Error message display / VI: Hiển thị thông báo lỗi API */}
{apiError && (
<div
className="p-3 rounded-lg bg-accent-error/10 border border-accent-error/20 text-accent-error text-sm flex items-center gap-2 animate-in fade-in duration-quick"
role="alert"
>
<svg
className="h-4 w-4 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<svg
className="h-4 w-4 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{apiError}</span>
</div>
)}
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{apiError}</span>
</div>
)}
<div className="space-y-4">
{/* EN: Email input field / VI: Trường nhập email */}
<Input
type="email"
@@ -156,79 +186,80 @@ export default function LoginPage() {
placeholder="you@example.com"
isInvalid={!!errors.email}
errorMessage={errors.email?.message}
name="email"
value={watch('email')}
onChange={(value) => setValue('email', value)}
onBlur={register('email').onBlur}
{...register('email')}
onChange={(e) => {
register('email').onChange(e);
setValue('email', e.target.value);
}}
autoComplete="email"
aria-required="true"
/>
{/* EN: Password input field / VI: Trường nhập mật khẩu */}
<Input
type="password"
label={t('auth.login.password')}
placeholder={t('auth.login.password')}
isInvalid={!!errors.password}
errorMessage={errors.password?.message}
name="password"
value={watch('password')}
onChange={(value) => setValue('password', value)}
onBlur={register('password').onBlur}
autoComplete="current-password"
aria-required="true"
/>
<div className="space-y-1">
<Input
type="password"
label={t('auth.login.password')}
placeholder={t('auth.login.password')}
isInvalid={!!errors.password}
errorMessage={errors.password?.message}
{...register('password')}
onChange={(e) => {
register('password').onChange(e);
setValue('password', e.target.value);
}}
autoComplete="current-password"
aria-required="true"
/>
{/* EN: Remember me and forgot password row / VI: Hàng nhớ đăng nhập và quên mật khẩu */}
<div className="flex items-center justify-between">
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
{...register('rememberMe')}
className="w-4 h-4 rounded border-border-primary bg-bg-secondary text-accent-primary focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary cursor-pointer"
/>
<span className="text-sm text-text-secondary group-hover:text-text-primary transition-colors">
{t('auth.login.rememberMe')}
</span>
</label>
<div className="flex items-center justify-between pt-2">
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
{...register('rememberMe')}
className="w-4 h-4 rounded border-glass bg-glass-subtle text-accent-primary focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary cursor-pointer transition-all"
/>
<span className="text-sm text-text-secondary group-hover:text-text-primary transition-colors">
{t('auth.login.rememberMe')}
</span>
</label>
<Link
href="/forgot-password"
className="text-sm text-accent-primary hover:brightness-110 transition-colors focus:outline-none focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary rounded"
>
{t('auth.login.forgotPassword')}
</Link>
<Link
href="/forgot-password"
className="text-sm font-medium text-accent-primary hover:text-accent-primary-hover transition-colors"
>
{t('auth.login.forgotPassword')}
</Link>
</div>
</div>
</CardContent>
</div>
<CardFooter className="flex flex-col gap-4">
{/* EN: Submit button with loading state / VI: Nút submit với trạng thái loading */}
<Button
type="submit"
variant="primary"
size="lg"
className="w-full"
isLoading={isLoading || isSubmitting}
isDisabled={isLoading || isSubmitting}
{/* EN: Submit button with loading state / VI: Nút submit với trạng thái loading */}
<Button
type="submit"
variant="brand"
size="lg"
className="w-full mt-4"
isLoading={isLoading || isSubmitting}
isDisabled={isLoading || isSubmitting}
>
{isLoading || isSubmitting
? t('auth.login.signingIn')
: t('auth.login.title')}
</Button>
{/* EN: Sign up link / VI: Link đăng ký */}
<p className="text-sm text-center text-text-tertiary pt-4 border-t border-glass-subtle">
{t('auth.login.noAccount')}{' '}
<Link
href="/register"
className="text-accent-primary hover:text-accent-primary-hover font-medium transition-colors"
>
{isLoading || isSubmitting
? t('auth.login.signingIn')
: t('auth.login.title')}
</Button>
{/* EN: Sign up link / VI: Link đăng ký */}
<p className="text-sm text-center text-text-tertiary">
{t('auth.login.noAccount')}{' '}
<Link
href="/register"
className="text-accent-primary hover:brightness-110 font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary rounded"
>
{t('auth.login.signUp')}
</Link>
</p>
</CardFooter>
{t('auth.login.signUp')}
</Link>
</p>
</form>
</Card>
</div>
</div>
</main>
);

View File

@@ -222,47 +222,79 @@ export default function RegisterPage() {
};
return (
// EN: Centered register form layout with dark mode background
// VI: Layout form đăng ký được căn giữa với nền dark mode
<>
// EN: Centered register form layout with cosmic background and glassmorphism
// VI: Layout form đăng ký với nền vũ trụ và hiệu ứng kínhmorphism
<main
role="main"
aria-label={t('auth.register.createAccount')}
className="min-h-screen flex items-center justify-center relative overflow-hidden bg-black py-12 px-4 sm:px-6 lg:px-8"
>
{/* EN: Cosmic Background Elements */}
{/* VI: Các thành phần nền vũ trụ */}
<div className="absolute inset-0 z-0 pointer-events-none">
<div className="absolute top-[-10%] right-[-10%] w-[40%] h-[40%] bg-accent-primary/5 blur-[120px] rounded-full animate-float opacity-50" />
<div
className="absolute bottom-[-10%] left-[-10%] w-[40%] h-[40%] bg-accent-primary/5 blur-[120px] rounded-full animate-float opacity-50"
style={{ animationDelay: '-2s' }}
/>
</div>
<AuthControls />
<div className="min-h-screen flex items-center justify-center bg-bg-primary px-4 py-12">
<Card className="w-full max-w-md" hover={false} bordered>
<CardHeader className="text-center">
<CardTitle className="text-2xl font-semibold">
{t('auth.register.createAccount')}
</CardTitle>
<CardDescription className="mt-2">
{t('auth.register.signUpToStart')}
</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit(onSubmit)}>
<CardContent className="space-y-4">
{/* EN: API Error message display / VI: Hiển thị thông báo lỗi API */}
{apiError && (
<div
className="p-3 rounded-md bg-bg-tertiary border border-accent-error text-accent-error text-sm flex items-center gap-2"
role="alert"
<div className="w-full max-w-md space-y-8 relative z-10 glass-appear">
<div className="text-center space-y-4">
<div className="flex justify-center">
<div className="p-3 rounded-2xl bg-white/5 border border-white/10 shadow-glass-sm animate-float">
<svg
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-white"
>
<path
d="M12 2L14.4 9.6H22L15.8 14.2L18.2 21.8L12 17.2L5.8 21.8L8.2 14.2L2 9.6H9.6L12 2Z"
fill="currentColor"
/>
</svg>
</div>
</div>
<h1 className="text-4xl font-bold tracking-tight text-white mb-2">
{t('auth.register.createAccount')}
</h1>
<p className="text-text-secondary">
{t('auth.register.signUpToStart')}
</p>
</div>
<div className="glass-card p-8 shadow-glass-xl border-glass-hover/20">
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
{/* EN: API Error message display / VI: Hiển thị thông báo lỗi API */}
{apiError && (
<div
className="p-3 rounded-lg bg-accent-error/10 border border-accent-error/20 text-accent-error text-sm flex items-center gap-2 animate-in fade-in duration-quick"
role="alert"
>
<svg
className="h-4 w-4 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<svg
className="h-4 w-4 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{apiError}</span>
</div>
)}
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{apiError}</span>
</div>
)}
<div className="space-y-4">
{/* EN: Full name input field / VI: Trường nhập họ tên */}
<Input
type="text"
@@ -270,10 +302,11 @@ export default function RegisterPage() {
placeholder="John Doe"
isInvalid={!!errors.fullName}
errorMessage={errors.fullName?.message}
name="fullName"
value={watch('fullName')}
onChange={(value) => setValue('fullName', value)}
onBlur={register('fullName').onBlur}
{...register('fullName')}
onChange={(e) => {
register('fullName').onChange(e);
setValue('fullName', e.target.value);
}}
autoComplete="name"
aria-required="true"
/>
@@ -285,58 +318,57 @@ export default function RegisterPage() {
placeholder="you@example.com"
isInvalid={!!errors.email}
errorMessage={errors.email?.message}
name="email"
value={watch('email')}
onChange={(value) => setValue('email', value)}
onBlur={register('email').onBlur}
{...register('email')}
onChange={(e) => {
register('email').onChange(e);
setValue('email', e.target.value);
}}
autoComplete="email"
aria-required="true"
/>
{/* EN: Password input field / VI: Trường nhập mật khẩu */}
<div>
<div className="space-y-2">
<Input
type="password"
label={t('auth.register.password')}
placeholder={t('auth.register.createStrongPassword')}
isInvalid={!!errors.password}
errorMessage={errors.password?.message}
name="password"
value={watch('password')}
onChange={(value) => setValue('password', value)}
onBlur={register('password').onBlur}
{...register('password')}
onChange={(e) => {
register('password').onChange(e);
setValue('password', e.target.value);
}}
autoComplete="new-password"
aria-required="true"
/>
{/* EN: Password strength indicator / VI: Chỉ báo độ mạnh mật khẩu */}
{password && (
<div className="mt-2 space-y-2">
{/* EN: Strength bar / VI: Thanh độ mạnh */}
<div className="w-full h-2 bg-bg-tertiary rounded-full overflow-hidden">
<div
className={`h-full transition-all duration-300 ${getStrengthColor(
passwordStrength.strength
)}`}
style={{ width: `${passwordStrength.percentage}%` }}
role="progressbar"
aria-valuenow={passwordStrength.percentage}
aria-valuemin={0}
aria-valuemax={100}
aria-label={t('auth.register.passwordStrength', { strength: t(`auth.register.${passwordStrength.feedback}`) })}
/>
<div className="space-y-2 px-1">
<div className="flex gap-1.5">
{[1, 2, 3, 4].map((step) => (
<div
key={step}
className={cn(
'h-1 flex-1 rounded-full transition-all duration-500',
passwordStrength.percentage >= step * 25
? getStrengthColor(passwordStrength.strength)
: 'bg-glass-subtle'
)}
/>
))}
</div>
{/* EN: Strength feedback text / VI: Text phản hồi độ mạnh */}
<p
className={`text-xs font-medium ${passwordStrength.strength === 'weak'
? 'text-accent-error'
: passwordStrength.strength === 'fair'
? 'text-accent-warning'
: passwordStrength.strength === 'good'
? 'text-accent-warning'
: 'text-accent-success'
}`}
className={cn(
'text-[10px] uppercase tracking-wider font-semibold',
passwordStrength.strength === 'weak'
? 'text-accent-error'
: passwordStrength.strength === 'strong'
? 'text-accent-success'
: 'text-accent-warning'
)}
>
{t(`auth.register.${passwordStrength.feedback}`)}
</p>
@@ -351,21 +383,22 @@ export default function RegisterPage() {
placeholder={t('auth.register.reEnterPassword')}
isInvalid={!!errors.confirmPassword}
errorMessage={errors.confirmPassword?.message}
name="confirmPassword"
value={watch('confirmPassword')}
onChange={(value) => setValue('confirmPassword', value)}
onBlur={register('confirmPassword').onBlur}
{...register('confirmPassword')}
onChange={(e) => {
register('confirmPassword').onChange(e);
setValue('confirmPassword', e.target.value);
}}
autoComplete="new-password"
aria-required="true"
/>
{/* EN: Terms and conditions checkbox / VI: Checkbox điều khoản và điều kiện */}
<div className="space-y-2">
<div className="space-y-2 pt-2">
<label className="flex items-start gap-2 cursor-pointer group">
<input
type="checkbox"
{...register('terms')}
className="mt-1 w-4 h-4 rounded border-border-primary bg-bg-secondary text-accent-primary focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary cursor-pointer flex-shrink-0"
className="mt-1 w-4 h-4 rounded border-glass bg-glass-subtle text-accent-primary focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary cursor-pointer flex-shrink-0 transition-all"
aria-required="true"
aria-invalid={errors.terms ? 'true' : 'false'}
/>
@@ -373,7 +406,7 @@ export default function RegisterPage() {
{t('auth.register.agreeToTerms')}{' '}
<Link
href="/terms"
className="text-accent-primary hover:brightness-110 underline focus:outline-none focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary rounded"
className="text-accent-primary hover:text-accent-primary-hover font-medium transition-colors"
target="_blank"
rel="noopener noreferrer"
>
@@ -382,56 +415,43 @@ export default function RegisterPage() {
</span>
</label>
{errors.terms && (
<p className="text-sm text-accent-error flex items-center gap-1 ml-6" role="alert">
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{errors.terms.message}
<p
className="text-sm text-accent-error flex items-center gap-1 ml-6 animate-in fade-in duration-quick"
role="alert"
>
<span>{errors.terms.message}</span>
</p>
)}
</div>
</CardContent>
</div>
<CardFooter className="flex flex-col gap-4">
{/* EN: Submit button with loading state / VI: Nút submit với trạng thái loading */}
<Button
type="submit"
variant="primary"
size="lg"
className="w-full"
isLoading={isLoading || isSubmitting}
isDisabled={isLoading || isSubmitting}
{/* EN: Submit button with loading state / VI: Nút submit với trạng thái loading */}
<Button
type="submit"
variant="brand"
size="lg"
className="w-full mt-4"
isLoading={isLoading || isSubmitting}
isDisabled={isLoading || isSubmitting}
>
{isLoading || isSubmitting
? t('auth.register.creatingAccount')
: t('auth.register.createAccount')}
</Button>
{/* EN: Sign in link / VI: Link đăng nhập */}
<p className="text-sm text-center text-text-tertiary pt-4 border-t border-glass-subtle">
{t('auth.register.alreadyHaveAccount')}{' '}
<Link
href="/login"
className="text-accent-primary hover:text-accent-primary-hover font-medium transition-colors"
>
{isLoading || isSubmitting
? t('auth.register.creatingAccount')
: t('auth.register.createAccount')}
</Button>
{/* EN: Sign in link / VI: Link đăng nhập */}
<p className="text-sm text-center text-text-tertiary">
{t('auth.register.alreadyHaveAccount')}{' '}
<Link
href="/login"
className="text-accent-primary hover:brightness-110 font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary rounded"
>
{t('auth.register.signIn')}
</Link>
</p>
</CardFooter>
{t('auth.register.signIn')}
</Link>
</p>
</form>
</Card>
</div>
</div>
</>
</main>
);
}

View File

@@ -15,7 +15,7 @@
'use client';
import React from 'react';
import React, { useState } from 'react';
import {
TextField as AriaTextField,
Label,
@@ -24,6 +24,8 @@ import {
type TextFieldProps as AriaTextFieldProps,
} from 'react-aria-components';
import { cn } from '@/shared/utils';
import { Eye, EyeOff } from 'lucide-react';
import { Button } from '../button/button';
export interface InputProps extends Omit<AriaTextFieldProps, 'children'> {
/**
@@ -130,6 +132,35 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
},
ref
) => {
// EN: Internal state for password visibility
// VI: State nội bộ cho việc hiển thị mật khẩu
const [showPassword, setShowPassword] = useState(false);
// EN: Determine the actual input type
// VI: Xác định type thực tế của input
const inputType = type === 'password' && showPassword ? 'text' : type;
// EN: Determine the right element for password toggle
// VI: Xác định element bên phải cho nút gạt mật khẩu
const finalRightElement =
type === 'password' ? (
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 hover:bg-white/10 text-text-tertiary"
onPress={() => setShowPassword(!showPassword)}
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
) : (
rightElement
);
return (
<AriaTextField
className={cn('group flex flex-col gap-1.5', className)}
@@ -160,7 +191,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
<AriaInput
ref={ref}
type={type}
type={inputType}
placeholder={placeholder}
className={cn(
// Base glass input styles
@@ -185,15 +216,15 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
leftElement && 'pl-10',
// Right element padding
rightElement && 'pr-10',
finalRightElement && 'pr-10',
inputClassName
)}
/>
{rightElement && (
{finalRightElement && (
<div className="absolute right-3 flex items-center text-text-tertiary">
{rightElement}
{finalRightElement}
</div>
)}
</div>