refactor: Cập nhật các trường nhập liệu form để sử dụng React Hook Form Controller và sửa lỗi không khớp hydration của nút chuyển đổi chủ đề.
This commit is contained in:
@@ -243,10 +243,12 @@ export default function ForgotPasswordPage() {
|
||||
placeholder="you@example.com"
|
||||
isInvalid={!!errors.email}
|
||||
errorMessage={errors.email?.message}
|
||||
{...register('email')}
|
||||
onChange={(e) => {
|
||||
register('email').onChange(e);
|
||||
setValue('email', e.target.value);
|
||||
name={register('email').name}
|
||||
onBlur={register('email').onBlur}
|
||||
onChange={(value) => {
|
||||
// EN: React Aria onChange receives value string, not event
|
||||
// VI: React Aria onChange nhận value string, không phải event
|
||||
setValue('email', value);
|
||||
}}
|
||||
autoComplete="email"
|
||||
autoFocus
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -70,9 +70,8 @@ export default function LoginPage() {
|
||||
// EN: React Hook Form setup with Zod resolver
|
||||
// VI: Setup React Hook Form với Zod resolver
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<LoginFormData>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
@@ -173,44 +172,60 @@ export default function LoginPage() {
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* EN: Email input field / VI: Trường nhập email */}
|
||||
<Input
|
||||
type="email"
|
||||
label={t('auth.login.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"
|
||||
aria-required="true"
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
type="email"
|
||||
label={t('auth.login.email')}
|
||||
placeholder="you@example.com"
|
||||
isInvalid={!!errors.email}
|
||||
errorMessage={errors.email?.message}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
autoComplete="email"
|
||||
aria-required="true"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* EN: Password input field / VI: Trường nhập mật khẩu */}
|
||||
<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"
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
type="password"
|
||||
label={t('auth.login.password')}
|
||||
placeholder={t('auth.login.password')}
|
||||
isInvalid={!!errors.password}
|
||||
errorMessage={errors.password?.message}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
autoComplete="current-password"
|
||||
aria-required="true"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<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"
|
||||
<Controller
|
||||
name="rememberMe"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.value}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
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')}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTheme } from '@/features/theme';
|
||||
import { Sun, Moon, Monitor } from 'lucide-react';
|
||||
import { Button } from '@/features/shared/components/ui/button';
|
||||
@@ -22,6 +22,7 @@ import { cn } from '@/shared/lib/utils';
|
||||
* - Icons from lucide-react
|
||||
* - Smooth transitions
|
||||
* - Keyboard accessible
|
||||
* - Prevents hydration mismatch
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
@@ -31,13 +32,23 @@ import { cn } from '@/shared/lib/utils';
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||
|
||||
// EN: Track mounted state to prevent hydration mismatch
|
||||
// VI: Theo dõi trạng thái mounted để tránh hydration mismatch
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const themeOptions = [
|
||||
{ value: 'light' as const, label: 'Light', icon: Sun },
|
||||
{ value: 'dark' as const, label: 'Dark', icon: Moon },
|
||||
{ value: 'system' as const, label: 'System', icon: Monitor },
|
||||
];
|
||||
|
||||
const currentIcon = resolvedTheme === 'dark' ? Moon : Sun;
|
||||
// EN: Use Monitor icon during SSR to prevent mismatch
|
||||
// VI: Sử dụng icon Monitor trong SSR để tránh mismatch
|
||||
const currentIcon = !mounted ? Monitor : (resolvedTheme === 'dark' ? Moon : Sun);
|
||||
const CurrentIcon = currentIcon;
|
||||
|
||||
return (
|
||||
@@ -95,7 +106,18 @@ export function ThemeToggle() {
|
||||
*/
|
||||
export function ThemeToggleButton() {
|
||||
const { toggleTheme, resolvedTheme } = useTheme();
|
||||
const Icon = resolvedTheme === 'dark' ? Sun : Moon;
|
||||
|
||||
// EN: Track mounted state to prevent hydration mismatch
|
||||
// VI: Theo dõi trạng thái mounted để tránh hydration mismatch
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// EN: Use Monitor icon during SSR to prevent mismatch
|
||||
// VI: Sử dụng icon Monitor trong SSR để tránh mismatch
|
||||
const Icon = !mounted ? Monitor : (resolvedTheme === 'dark' ? Sun : Moon);
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user