**Authentication Refactor:** Remove authStore and its associated tests, transitioning to the new authentication model. Add dynamic loading for PasswordResetConfirmForm to optimize performance. Include a theme initialization script in layout.tsx to prevent FOUC.
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { useAuthStore } from '@/lib/stores/authStore';
|
||||
import { useMe } from '@/lib/api/hooks/useAuth';
|
||||
import config from '@/config/app.config';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { useAuthStore } from '@/lib/stores/authStore';
|
||||
|
||||
/**
|
||||
* AuthInitializer - Initializes auth state from encrypted storage on mount
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// Authentication components
|
||||
|
||||
// Initialization
|
||||
export { AuthInitializer } from './AuthInitializer';
|
||||
|
||||
// Route protection
|
||||
export { AuthGuard } from './AuthGuard';
|
||||
|
||||
|
||||
101
frontend/src/components/forms/FormField.tsx
Normal file
101
frontend/src/components/forms/FormField.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* FormField Component
|
||||
* Reusable form field with integrated label, input, and error display
|
||||
* Designed for react-hook-form with proper accessibility attributes
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { ComponentProps, ReactNode } from 'react';
|
||||
import { FieldError } from 'react-hook-form';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
export interface FormFieldProps extends Omit<ComponentProps<typeof Input>, 'children'> {
|
||||
/** Field label text */
|
||||
label: string;
|
||||
/** Field name/id - optional if provided via register() */
|
||||
name?: string;
|
||||
/** Is field required? Shows asterisk if true */
|
||||
required?: boolean;
|
||||
/** Form error object from react-hook-form */
|
||||
error?: FieldError;
|
||||
/** Label description or helper text */
|
||||
description?: string;
|
||||
/** Additional content after input (e.g., password requirements) */
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* FormField - Standardized form field with label and error handling
|
||||
*
|
||||
* Features:
|
||||
* - Automatic error ID generation for accessibility
|
||||
* - Required indicator
|
||||
* - Error message display
|
||||
* - Helper text/description support
|
||||
* - Full ARIA attribute support
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <FormField
|
||||
* label="Email"
|
||||
* name="email"
|
||||
* type="email"
|
||||
* required
|
||||
* error={form.formState.errors.email}
|
||||
* disabled={isSubmitting}
|
||||
* {...form.register('email')}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export function FormField({
|
||||
label,
|
||||
name: explicitName,
|
||||
required = false,
|
||||
error,
|
||||
description,
|
||||
children,
|
||||
...inputProps
|
||||
}: FormFieldProps) {
|
||||
// Extract name from inputProps (from register()) or use explicit name
|
||||
// register() adds a name property that may not be in the type
|
||||
const registerName = ('name' in inputProps) ? (inputProps as { name: string }).name : undefined;
|
||||
const name = explicitName || registerName;
|
||||
|
||||
if (!name) {
|
||||
throw new Error('FormField: name must be provided either explicitly or via register()');
|
||||
}
|
||||
|
||||
const errorId = error ? `${name}-error` : undefined;
|
||||
const descriptionId = description ? `${name}-description` : undefined;
|
||||
const ariaDescribedBy = [errorId, descriptionId].filter(Boolean).join(' ') || undefined;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{label && (
|
||||
<Label htmlFor={name}>
|
||||
{label}
|
||||
{required && <span className="text-destructive"> *</span>}
|
||||
</Label>
|
||||
)}
|
||||
{description && (
|
||||
<p id={descriptionId} className="text-sm text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
<Input
|
||||
id={name}
|
||||
aria-invalid={!!error}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
{...inputProps}
|
||||
/>
|
||||
{error && (
|
||||
<p id={errorId} className="text-sm text-destructive" role="alert">
|
||||
{error.message}
|
||||
</p>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
frontend/src/components/forms/index.ts
Normal file
5
frontend/src/components/forms/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Shared form components and utilities
|
||||
export { FormField } from './FormField';
|
||||
export type { FormFieldProps } from './FormField';
|
||||
export { useFormError } from './useFormError';
|
||||
export type { UseFormErrorReturn } from './useFormError';
|
||||
91
frontend/src/components/forms/useFormError.ts
Normal file
91
frontend/src/components/forms/useFormError.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* useFormError Hook
|
||||
* Handles server error state and API error parsing for forms
|
||||
* Standardizes error handling across all form components
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { UseFormReturn, FieldValues, Path } from 'react-hook-form';
|
||||
import { getGeneralError, getFieldErrors, isAPIErrorArray } from '@/lib/api/errors';
|
||||
|
||||
export interface UseFormErrorReturn {
|
||||
/** Current server error message */
|
||||
serverError: string | null;
|
||||
/** Set server error manually */
|
||||
setServerError: (error: string | null) => void;
|
||||
/** Handle API error and update form with field-specific errors */
|
||||
handleFormError: (error: unknown) => void;
|
||||
/** Clear all errors */
|
||||
clearErrors: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* useFormError - Standardized form error handling
|
||||
*
|
||||
* Features:
|
||||
* - Server error state management
|
||||
* - API error parsing with type guards
|
||||
* - Automatic field error mapping to react-hook-form
|
||||
* - General error message extraction
|
||||
*
|
||||
* @param form - react-hook-form instance
|
||||
* @returns Error handling utilities
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const form = useForm<LoginFormData>({...});
|
||||
* const { serverError, handleFormError, clearErrors } = useFormError(form);
|
||||
*
|
||||
* const onSubmit = async (data: LoginFormData) => {
|
||||
* try {
|
||||
* clearErrors();
|
||||
* await loginMutation.mutateAsync(data);
|
||||
* } catch (error) {
|
||||
* handleFormError(error);
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export function useFormError<TFieldValues extends FieldValues>(
|
||||
form: UseFormReturn<TFieldValues>
|
||||
): UseFormErrorReturn {
|
||||
const [serverError, setServerError] = useState<string | null>(null);
|
||||
|
||||
const handleFormError = useCallback(
|
||||
(error: unknown) => {
|
||||
// Handle API errors with type guard
|
||||
if (isAPIErrorArray(error)) {
|
||||
// Set general error message
|
||||
const generalError = getGeneralError(error);
|
||||
if (generalError) {
|
||||
setServerError(generalError);
|
||||
}
|
||||
|
||||
// Set field-specific errors
|
||||
const fieldErrors = getFieldErrors(error);
|
||||
Object.entries(fieldErrors).forEach(([field, message]) => {
|
||||
// Check if field exists in form values to avoid setting invalid fields
|
||||
if (field in form.getValues()) {
|
||||
form.setError(field as Path<TFieldValues>, { message });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Unexpected error format
|
||||
setServerError('An unexpected error occurred. Please try again.');
|
||||
}
|
||||
},
|
||||
[form]
|
||||
);
|
||||
|
||||
const clearErrors = useCallback(() => {
|
||||
setServerError(null);
|
||||
form.clearErrors();
|
||||
}, [form]);
|
||||
|
||||
return {
|
||||
serverError,
|
||||
setServerError,
|
||||
handleFormError,
|
||||
clearErrors,
|
||||
};
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { useAuthStore } from '@/lib/stores/authStore';
|
||||
import { useLogout } from '@/lib/api/hooks/useAuth';
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
||||
Reference in New Issue
Block a user