extract auth to lib

This commit is contained in:
2026-02-22 20:37:30 +01:00
parent db6813cab1
commit 9f86fe80d7
24 changed files with 2442 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
import { useCallback, useMemo, useState } from 'react';
type FieldErrors<TValues> = Partial<Record<keyof TValues, string | undefined>>;
type TouchedFields<TValues> = Partial<Record<keyof TValues, boolean>>;
type SetValuesOptions = {
validate?: boolean;
clearErrors?: boolean;
};
type SetFieldValueOptions = {
validate?: boolean;
touch?: boolean;
};
type ValidateAllOptions = {
touchAll?: boolean;
};
type UseValidatedFieldsOptions<TValues> = {
initialValues: TValues;
validate: (values: TValues) => FieldErrors<TValues>;
};
function hasErrors<TValues>(errors: FieldErrors<TValues>): boolean {
return Object.values(errors).some(Boolean);
}
function pickTouchedErrors<TValues>(
errors: FieldErrors<TValues>,
touched: TouchedFields<TValues>
): FieldErrors<TValues> {
const next: FieldErrors<TValues> = {};
for (const key of Object.keys(errors) as Array<keyof TValues>) {
if (touched[key]) {
next[key] = errors[key];
}
}
return next;
}
function touchAll<TValues extends Record<string, string>>(values: TValues): TouchedFields<TValues> {
const touched: TouchedFields<TValues> = {};
for (const key of Object.keys(values) as Array<keyof TValues>) {
touched[key] = true;
}
return touched;
}
export function useValidatedFields<TValues extends Record<string, string>>({
initialValues,
validate
}: UseValidatedFieldsOptions<TValues>) {
const [values, setValues] = useState<TValues>(initialValues);
const [allErrors, setAllErrors] = useState<FieldErrors<TValues>>(() => validate(initialValues));
const [touched, setTouched] = useState<TouchedFields<TValues>>({});
const updateValues = useCallback((nextValues: TValues, options: SetValuesOptions = {}) => {
const { validate: shouldValidate = false, clearErrors = false } = options;
setValues(nextValues);
if (shouldValidate || clearErrors) {
setAllErrors(validate(nextValues));
}
if (clearErrors) {
setTouched({});
}
}, [validate]);
const setFieldValue = useCallback(<K extends keyof TValues>(
key: K,
value: TValues[K],
options: SetFieldValueOptions = {}
) => {
const { validate: shouldValidate = true, touch = true } = options;
if (touch) {
setTouched((current) => ({
...current,
[key]: true
}));
}
setValues((current) => {
const nextValues = {
...current,
[key]: value
};
if (shouldValidate) {
setAllErrors(validate(nextValues));
}
return nextValues;
});
}, [validate]);
const validateAll = useCallback((options: ValidateAllOptions = {}) => {
const { touchAll: shouldTouchAll = true } = options;
const nextErrors = validate(values);
setAllErrors(nextErrors);
if (shouldTouchAll) {
setTouched(touchAll(values));
}
return nextErrors;
}, [validate, values]);
const setFieldError = useCallback(<K extends keyof TValues>(key: K, message?: string) => {
setTouched((current) => ({
...current,
[key]: true
}));
setAllErrors((current) => ({
...current,
[key]: message
}));
}, []);
const updateErrors = useCallback((nextErrors: FieldErrors<TValues>) => {
const nextTouched: TouchedFields<TValues> = {};
for (const key of Object.keys(nextErrors) as Array<keyof TValues>) {
if (nextErrors[key]) {
nextTouched[key] = true;
}
}
setTouched((current) => ({
...current,
...nextTouched
}));
setAllErrors(nextErrors);
}, []);
const clearErrors = useCallback(() => {
setAllErrors(validate(values));
setTouched({});
}, [validate, values]);
const errors = useMemo(() => pickTouchedErrors(allErrors, touched), [allErrors, touched]);
const isValid = useMemo(() => {
return !hasErrors(validate(values));
}, [validate, values]);
return {
values,
errors,
isValid,
setValues: updateValues,
setFieldValue,
validateAll,
setFieldError,
setErrors: updateErrors,
clearErrors
};
}