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:
Ho Ngoc Hai
2026-01-05 09:58:56 +07:00
parent 62a008e8b1
commit 29de865509
3 changed files with 79 additions and 40 deletions

View File

@@ -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

View File

@@ -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')}

View File

@@ -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