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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user