cleanup forms

This commit is contained in:
Ildar Kamalov
2025-01-21 15:31:08 +03:00
parent 290987d020
commit edd9bf7b59
9 changed files with 293 additions and 301 deletions

View File

@@ -1,12 +1,12 @@
import React, { ComponentProps, forwardRef, ReactNode } from 'react'; import React, { ComponentProps, forwardRef, ReactNode } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
interface Props extends ComponentProps<'input'> { type Props = ComponentProps<'input'> & {
label?: string; label?: string;
leftAddon?: ReactNode; leftAddon?: ReactNode;
rightAddon?: ReactNode; rightAddon?: ReactNode;
error?: string; error?: string;
} };
export const Input = forwardRef<HTMLInputElement, Props>( export const Input = forwardRef<HTMLInputElement, Props>(
({ name, label, className, leftAddon, rightAddon, error, ...rest }, ref) => ( ({ name, label, className, leftAddon, rightAddon, error, ...rest }, ref) => (
@@ -18,10 +18,10 @@ export const Input = forwardRef<HTMLInputElement, Props>(
)} )}
<div className="input-group"> <div className="input-group">
{leftAddon && <div>{leftAddon}</div>} {leftAddon && <div>{leftAddon}</div>}
<input className={clsx('form-control', className)} ref={ref} {...rest} /> <input className={clsx('form-control', { 'is-invalid': !!error }, className)} ref={ref} {...rest} />
{rightAddon && <div>{rightAddon}</div>} {rightAddon && <div>{rightAddon}</div>}
</div> </div>
{error && <div className="form__message form__message--error">{error}</div>} {error && <div className="form__message form__message--error mt-1">{error}</div>}
</div> </div>
), ),
); );

View File

@@ -0,0 +1,27 @@
import React, { ComponentProps, forwardRef } from 'react';
import clsx from 'clsx';
type SelectProps = ComponentProps<'select'> & {
label?: string;
error?: string;
};
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
({ name, label, className, error, children, ...rest }, ref) => (
<div className={clsx('form-group', { 'has-error': !!error })}>
{label && (
<label className="form__label" htmlFor={name}>
{label}
</label>
)}
<div className="input-group">
<select className={clsx('form-control custom-select', className)} ref={ref} {...rest}>
{children}
</select>
</div>
{error && <div className="form__message form__message--error mt-1">{error}</div>}
</div>
),
);
Select.displayName = 'Select';

View File

@@ -1,11 +1,11 @@
import React, { ComponentProps, forwardRef } from 'react'; import React, { ComponentProps, forwardRef } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
interface Props extends ComponentProps<'textarea'> { type Props = ComponentProps<'textarea'> & {
className?: string; className?: string;
label?: string; label?: string;
error?: string; error?: string;
} };
export const Textarea = forwardRef<HTMLTextAreaElement, Props>(({ name, label, className, error, ...rest }, ref) => ( export const Textarea = forwardRef<HTMLTextAreaElement, Props>(({ name, label, className, error, ...rest }, ref) => (
<div className={clsx('form-group', { 'has-error': !!error })}> <div className={clsx('form-group', { 'has-error': !!error })}>

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Trans } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import i18next from 'i18next'; import i18next from 'i18next';
import cn from 'classnames'; import cn from 'classnames';
@@ -13,6 +13,7 @@ import {
validatePort, validatePort,
validateIsSafePort, validateIsSafePort,
} from '../../../helpers/validators'; } from '../../../helpers/validators';
import { Input } from '../Controls/Input';
const getDownloadLink = (host: string, clientId: string, protocol: string, invalid: boolean) => { const getDownloadLink = (host: string, clientId: string, protocol: string, invalid: boolean) => {
if (!host || invalid) { if (!host || invalid) {
@@ -23,7 +24,7 @@ const getDownloadLink = (host: string, clientId: string, protocol: string, inval
); );
} }
const linkParams: { host: string, client_id?: string } = { host }; const linkParams: { host: string; client_id?: string } = { host };
if (clientId) { if (clientId) {
linkParams.client_id = clientId; linkParams.client_id = clientId;
@@ -33,8 +34,7 @@ const getDownloadLink = (host: string, clientId: string, protocol: string, inval
<a <a
href={getPathWithQueryString(protocol, linkParams)} href={getPathWithQueryString(protocol, linkParams)}
className={cn('btn btn-success btn-standard btn-large')} className={cn('btn btn-success btn-standard btn-large')}
download download>
>
{i18next.t('download_mobileconfig')} {i18next.t('download_mobileconfig')}
</a> </a>
); );
@@ -51,24 +51,32 @@ type FormValues = {
clientId: string; clientId: string;
protocol: string; protocol: string;
port?: number; port?: number;
} };
type Props = { type Props = {
initialValues?: FormValues; initialValues?: FormValues;
} };
const defaultFormValues = {
host: '',
clientId: '',
protocol: MOBILE_CONFIG_LINKS.DOT,
port: undefined,
};
export const MobileConfigForm = ({ initialValues }: Props) => { export const MobileConfigForm = ({ initialValues }: Props) => {
const { t } = useTranslation();
const { const {
register, register,
watch, watch,
formState: { errors, isValid }, control,
} = useForm<FormValues>({ formState: { isValid },
} = useForm<FormValues>({
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
host: initialValues?.host || '', ...defaultFormValues,
clientId: initialValues?.clientId || '', ...initialValues,
protocol: initialValues?.protocol || MOBILE_CONFIG_LINKS.DOT,
port: initialValues?.port || undefined,
}, },
}); });
@@ -91,50 +99,46 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
<div className="row"> <div className="row">
<div className="col"> <div className="col">
<label htmlFor="host" className="form__label"> <Controller
{i18next.t('dhcp_table_hostname')} name="host"
</label> control={control}
rules={{ validate: validateServerName }}
<input render={({ field, fieldState }) => (
id="host" <Input
type="text" {...field}
className="form-control" type="text"
placeholder={i18next.t('form_enter_hostname')} label={t('dhcp_table_hostname')}
{...register('host', { placeholder={t('form_enter_hostname')}
validate: validateServerName, error={fieldState.error?.message}
})} />
)}
/> />
{errors.host && (
<div className="form__message form__message--error">
{errors.host.message}
</div>
)}
</div> </div>
{protocol === MOBILE_CONFIG_LINKS.DOH && ( {protocol === MOBILE_CONFIG_LINKS.DOH && (
<div className="col"> <div className="col">
<label htmlFor="port" className="form__label"> <Controller
{i18next.t('encryption_https')} name="port"
</label> control={control}
rules={{
<input
id="port"
type="number"
className="form-control"
placeholder={i18next.t('encryption_https')}
{...register('port', {
setValueAs: (val) => toNumber(val),
validate: { validate: {
range: (value) => validatePort(value) || true, range: (value) => validatePort(value) || true,
safety: (value) => validateIsSafePort(value) || true, safety: (value) => validateIsSafePort(value) || true,
}, },
})} }}
render={({ field, fieldState }) => (
<Input
{...field}
type="number"
label={t('encryption_https')}
placeholder={t('encryption_https')}
error={fieldState.error?.message}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/> />
{errors.port && (
<div className="form__message form__message--error">
{errors.port.message}
</div>
)}
</div> </div>
)} )}
</div> </div>
@@ -149,21 +153,21 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
<Trans components={{ a: githubLink }}>client_id_desc</Trans> <Trans components={{ a: githubLink }}>client_id_desc</Trans>
</div> </div>
<input <Controller
id="clientId" name="clientId"
type="text" control={control}
className="form-control" rules={{
placeholder={i18next.t('client_id_placeholder')}
{...register('clientId', {
validate: validateConfigClientId, validate: validateConfigClientId,
})} }}
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
placeholder={t('client_id_placeholder')}
error={fieldState.error?.message}
/>
)}
/> />
{errors.clientId && (
<div className="form__message form__message--error">
{errors.clientId.message}
</div>
)}
</div> </div>
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
@@ -171,11 +175,7 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
{i18next.t('protocol')} {i18next.t('protocol')}
</label> </label>
<select <select id="protocol" className="form-control" {...register('protocol')}>
id="protocol"
className="form-control"
{...register('protocol')}
>
<option value={MOBILE_CONFIG_LINKS.DOT}>{i18next.t('dns_over_tls')}</option> <option value={MOBILE_CONFIG_LINKS.DOT}>{i18next.t('dns_over_tls')}</option>
<option value={MOBILE_CONFIG_LINKS.DOH}>{i18next.t('dns_over_https')}</option> <option value={MOBILE_CONFIG_LINKS.DOH}>{i18next.t('dns_over_https')}</option>
</select> </select>

View File

@@ -91,6 +91,7 @@ export const STANDARD_WEB_PORT = 80;
export const STANDARD_HTTPS_PORT = 443; export const STANDARD_HTTPS_PORT = 443;
export const DNS_OVER_TLS_PORT = 853; export const DNS_OVER_TLS_PORT = 853;
export const DNS_OVER_QUIC_PORT = 853; export const DNS_OVER_QUIC_PORT = 853;
export const MIN_PORT = 1;
export const MAX_PORT = 65535; export const MAX_PORT = 65535;
export const EMPTY_DATE = '0001-01-01T00:00:00Z'; export const EMPTY_DATE = '0001-01-01T00:00:00Z';

View File

@@ -1,27 +1,28 @@
import React from 'react'; import React from 'react';
import { useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { withTranslation, Trans } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import cn from 'classnames';
import i18n from '../../i18n';
import Controls from './Controls'; import Controls from './Controls';
import { validatePasswordLength } from '../../helpers/validators'; import { validatePasswordLength, validateRequiredValue } from '../../helpers/validators';
import { Input } from '../../components/ui/Controls/Input';
type AuthFormValues = {
username: string;
password: string;
confirm_password: string;
};
type Props = { type Props = {
onAuthSubmit: (...args: unknown[]) => string; onAuthSubmit: (values: AuthFormValues) => void;
pristine: boolean; };
invalid: boolean;
t: (...args: unknown[]) => string;
}
const Auth = (props: Props) => { export const Auth = ({ onAuthSubmit }: Props) => {
const { t, onAuthSubmit } = props; const { t } = useTranslation();
const { const {
register,
handleSubmit, handleSubmit,
watch, watch,
formState: { errors, isDirty, isValid }, control,
} = useForm({ formState: { isDirty, isValid },
} = useForm<AuthFormValues>({
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
username: '', username: '',
@@ -34,7 +35,7 @@ const Auth = (props: Props) => {
const validateConfirmPassword = (value: string) => { const validateConfirmPassword = (value: string) => {
if (value !== password) { if (value !== password) {
return i18n.t('form_error_password'); return t('form_error_password');
} }
return undefined; return undefined;
}; };
@@ -51,74 +52,71 @@ const Auth = (props: Props) => {
</p> </p>
<div className="form-group"> <div className="form-group">
<label> <Controller
<Trans>install_auth_username</Trans> name="username"
</label> control={control}
<input rules={{ validate: validateRequiredValue }}
{...register('username', { required: t('form_error_required') })} render={({ field, fieldState }) => (
type="text" <Input
className={cn('form-control', { 'is-invalid': errors.username })} {...field}
placeholder={t('install_auth_username_enter')} type="text"
autoComplete="username" label={t('install_auth_username')}
placeholder={t('install_auth_username_enter')}
error={fieldState.error?.message}
autoComplete="username"
/>
)}
/> />
{errors.username && (
<div className="form__message form__message--error">
{errors.username.message}
</div>
)}
</div> </div>
<div className="form-group"> <div className="form-group">
<label> <Controller
<Trans>install_auth_password</Trans> name="password"
</label> control={control}
<input rules={{
{...register('password', { validate: {
required: t('form_error_required'), required: validateRequiredValue,
validate: validatePasswordLength, passwordLength: validatePasswordLength,
})} },
type="password" }}
className={cn('form-control', { 'is-invalid': errors.password })} render={({ field, fieldState }) => (
placeholder={t('install_auth_password_enter')} <Input
autoComplete="new-password" {...field}
type="password"
label={t('install_auth_password')}
placeholder={t('install_auth_password_enter')}
error={fieldState.error?.message}
autoComplete="new-password"
/>
)}
/> />
{errors.password && (
<div className="form__message form__message--error">
{errors.password.message}
</div>
)}
</div> </div>
<div className="form-group"> <div className="form-group">
<label> <Controller
<Trans>install_auth_confirm</Trans> name="confirm_password"
</label> control={control}
<input rules={{
{...register('confirm_password', { validate: {
required: t('form_error_required'), required: validateRequiredValue,
validate: validateConfirmPassword, confirmPassword: validateConfirmPassword,
})} },
type="password" }}
className={cn('form-control', { 'is-invalid': errors.confirm_password })} render={({ field, fieldState }) => (
placeholder={t('install_auth_confirm')} <Input
autoComplete="new-password" {...field}
type="password"
label={t('install_auth_confirm')}
placeholder={t('install_auth_confirm')}
error={fieldState.error?.message}
autoComplete="new-password"
/>
)}
/> />
{errors.confirm_password && (
<div className="invalid-feedback">
{errors.confirm_password.message}
</div>
)}
</div> </div>
</div> </div>
<Controls <Controls isDirty={isDirty} isValid={isValid} />
isDirty={isDirty}
isValid={isValid}
/>
</form> </form>
); );
}; };
export default flow([
withTranslation(),
])(Auth);

View File

@@ -15,14 +15,17 @@ import {
STANDARD_DNS_PORT, STANDARD_DNS_PORT,
STANDARD_WEB_PORT, STANDARD_WEB_PORT,
MAX_PORT, MAX_PORT,
MIN_PORT,
} from '../../helpers/constants'; } from '../../helpers/constants';
import { toNumber } from '../../helpers/form';
import { validateRequiredValue } from '../../helpers/validators'; import { validateRequiredValue } from '../../helpers/validators';
import { DhcpInterface } from '../../initialState'; import { DhcpInterface } from '../../initialState';
import { Input } from '../../components/ui/Controls/Input';
import { Select } from '../../components/ui/Controls/Select';
import { toNumber } from '../../helpers/form';
const validateInstallPort = (value: any) => { const validateInstallPort = (value: number) => {
if (value < 1 || value > MAX_PORT) { if (value < MIN_PORT || value > MAX_PORT) {
return i18n.t('form_error_port'); return i18n.t('form_error_port');
} }
return undefined; return undefined;
@@ -77,13 +80,7 @@ const renderInterfaces = (interfaces: DhcpInterface[]) =>
return null; return null;
}); });
const Settings: React.FC<Props> = ({ export const Settings = ({ handleSubmit, handleFix, validateForm, config, interfaces }: Props) => {
handleSubmit,
handleFix,
validateForm,
config,
interfaces,
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const defaultValues = { const defaultValues = {
@@ -101,7 +98,7 @@ const Settings: React.FC<Props> = ({
control, control,
watch, watch,
handleSubmit: reactHookFormSubmit, handleSubmit: reactHookFormSubmit,
formState: { isValid, errors }, formState: { isValid },
} = useForm({ } = useForm({
defaultValues, defaultValues,
mode: 'onChange', mode: 'onChange',
@@ -113,12 +110,16 @@ const Settings: React.FC<Props> = ({
const { status: dnsStatus, can_autofix: isDnsFixAvailable } = config.dns; const { status: dnsStatus, can_autofix: isDnsFixAvailable } = config.dns;
const { staticIp } = config; const { staticIp } = config;
const webIpVal = watch("web.ip"); const webIpVal = watch('web.ip');
const webPortVal = watch("web.port"); const webPortVal = watch('web.port');
const dnsIpVal = watch("dns.ip"); const dnsIpVal = watch('dns.ip');
const dnsPortVal = watch("dns.port"); const dnsPortVal = watch('dns.port');
useEffect(() => { useEffect(() => {
if (!isValid || validateInstallPort(webPortVal) || validateInstallPort(dnsPortVal)) {
return;
}
validateForm({ validateForm({
web: { web: {
ip: webIpVal, ip: webIpVal,
@@ -171,44 +172,46 @@ const Settings: React.FC<Props> = ({
} }
}; };
const getStaticIpMessage = useCallback((staticIp: StaticIpType) => { const getStaticIpMessage = useCallback(
const { static: status, ip } = staticIp; (staticIp: StaticIpType) => {
const { static: status, ip } = staticIp;
switch (status) { switch (status) {
case STATUS_RESPONSE.NO: case STATUS_RESPONSE.NO:
return ( return (
<> <>
<div className="mb-2"> <div className="mb-2">
<Trans values={{ ip }} components={[<strong key="0">text</strong>]}> <Trans values={{ ip }} components={[<strong key="0">text</strong>]}>
install_static_configure install_static_configure
</Trans> </Trans>
</div>
<button
type="button"
className="btn btn-outline-primary btn-sm"
onClick={() => handleStaticIp(ip)}>
<Trans>set_static_ip</Trans>
</button>
</>
);
case STATUS_RESPONSE.ERROR:
return (
<div className="text-danger">
<Trans>install_static_error</Trans>
</div> </div>
);
<button case STATUS_RESPONSE.YES:
type="button" return (
className="btn btn-outline-primary btn-sm" <div className="text-success">
onClick={() => handleStaticIp(ip)} <Trans>install_static_ok</Trans>
> </div>
<Trans>set_static_ip</Trans> );
</button> default:
</> return null;
); }
case STATUS_RESPONSE.ERROR: },
return ( [handleStaticIp],
<div className="text-danger"> );
<Trans>install_static_error</Trans>
</div>
);
case STATUS_RESPONSE.YES:
return (
<div className="text-success">
<Trans>install_static_ok</Trans>
</div>
);
default:
return null;
}
}, [handleStaticIp]);
const onSubmit = (data: any) => { const onSubmit = (data: any) => {
validateForm(data); validateForm(data);
@@ -232,17 +235,12 @@ const Settings: React.FC<Props> = ({
name="web.ip" name="web.ip"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<select <Select {...field}>
{...field}
className="form-control custom-select"
onChange={(e) => {
field.onChange(e);
}}>
<option value={ALL_INTERFACES_IP}> <option value={ALL_INTERFACES_IP}>
{t('install_settings_all_interfaces')} {t('install_settings_all_interfaces')}
</option> </option>
{renderInterfaces(interfaces)} {renderInterfaces(interfaces)}
</select> </Select>
)} )}
/> />
</div> </div>
@@ -257,29 +255,24 @@ const Settings: React.FC<Props> = ({
name="web.port" name="web.port"
control={control} control={control}
rules={{ rules={{
required: t('form_error_required'),
validate: { validate: {
required: validateRequiredValue,
installPort: validateInstallPort, installPort: validateInstallPort,
}, },
}} }}
render={({ field }) => ( render={({ field, fieldState }) => (
<input <Input
{...field} {...field}
type="number" type="number"
className="form-control"
placeholder={STANDARD_WEB_PORT.toString()} placeholder={STANDARD_WEB_PORT.toString()}
error={fieldState.error?.message}
onChange={(e) => { onChange={(e) => {
const val = toNumber(e.target.value); const { value } = e.target;
field.onChange(val); field.onChange(toNumber(value));
}} }}
/> />
)} )}
/> />
{errors.web?.port && (
<div className="form__message form__message--error">
{errors.web.port.message}
</div>
)}
</div> </div>
</div> </div>
@@ -291,8 +284,7 @@ const Settings: React.FC<Props> = ({
<button <button
type="button" type="button"
className="btn btn-secondary btn-sm ml-2" className="btn btn-secondary btn-sm ml-2"
onClick={() => handleAutofix('web')} onClick={() => handleAutofix('web')}>
>
<Trans>fix</Trans> <Trans>fix</Trans>
</button> </button>
)} )}
@@ -331,17 +323,12 @@ const Settings: React.FC<Props> = ({
name="dns.ip" name="dns.ip"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<select <Select {...field}>
{...field}
className="form-control custom-select"
onChange={(e) => {
field.onChange(e);
}}>
<option value={ALL_INTERFACES_IP}> <option value={ALL_INTERFACES_IP}>
{t('install_settings_all_interfaces')} {t('install_settings_all_interfaces')}
</option> </option>
{renderInterfaces(interfaces)} {renderInterfaces(interfaces)}
</select> </Select>
)} )}
/> />
</div> </div>
@@ -362,24 +349,19 @@ const Settings: React.FC<Props> = ({
installPort: validateInstallPort, installPort: validateInstallPort,
}, },
}} }}
render={({ field }) => ( render={({ field, fieldState }) => (
<input <Input
{...field} {...field}
type="number" type="number"
className="form-control" error={fieldState.error?.message}
placeholder={STANDARD_WEB_PORT.toString()} placeholder={STANDARD_WEB_PORT.toString()}
onChange={(e) => { onChange={(e) => {
const val = toNumber(e.target.value); const { value } = e.target;
field.onChange(val); field.onChange(toNumber(value));
}} }}
/> />
)} )}
/> />
{errors.dns?.port.message && (
<div className="form__message form__message--error">
{t(errors.dns.port.message)}
</div>
)}
</div> </div>
</div> </div>
@@ -415,16 +397,10 @@ const Settings: React.FC<Props> = ({
dnsStatus?.includes(ADDRESS_IN_USE_TEXT) && ( dnsStatus?.includes(ADDRESS_IN_USE_TEXT) && (
<Trans <Trans
components={[ components={[
<a <a href={PORT_53_FAQ_LINK} key="0" target="_blank" rel="noopener noreferrer">
href={PORT_53_FAQ_LINK}
key="0"
target="_blank"
rel="noopener noreferrer"
>
link link
</a>, </a>,
]} ]}>
>
port_53_faq_link port_53_faq_link
</Trans> </Trans>
)} )}
@@ -463,5 +439,3 @@ const Settings: React.FC<Props> = ({
</form> </form>
); );
}; };
export default Settings;

View File

@@ -9,10 +9,11 @@ import { INSTALL_TOTAL_STEPS, ALL_INTERFACES_IP, DEBOUNCE_TIMEOUT } from '../../
import Loading from '../../components/ui/Loading'; import Loading from '../../components/ui/Loading';
import Greeting from './Greeting'; import Greeting from './Greeting';
import Settings from './Settings'; import { Settings } from './Settings';
import Devices from './Devices'; import Devices from './Devices';
import Submit from './Submit'; import Submit from './Submit';
import Progress from './Progress'; import Progress from './Progress';
import { Auth } from './Auth';
import Toasts from '../../components/Toasts'; import Toasts from '../../components/Toasts';
import Footer from '../../components/ui/Footer'; import Footer from '../../components/ui/Footer';
import Icons from '../../components/ui/Icons'; import Icons from '../../components/ui/Icons';
@@ -20,7 +21,6 @@ import { Logo } from '../../components/ui/svg/logo';
import './Setup.css'; import './Setup.css';
import '../../components/ui/Tabler.css'; import '../../components/ui/Tabler.css';
import Auth from './Auth';
const Setup = () => { const Setup = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -37,11 +37,13 @@ const Setup = () => {
delete config.staticIp; delete config.staticIp;
if (web.port && dns.port) { if (web.port && dns.port) {
dispatch(actionCreators.setAllSettings({ dispatch(
web, actionCreators.setAllSettings({
dns, web,
...config, dns,
})); ...config,
}),
);
} }
}; };
@@ -120,4 +122,4 @@ const Setup = () => {
); );
}; };
export default Setup; export default Setup;

View File

@@ -1,23 +1,25 @@
import React from 'react'; import React from 'react';
import { useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Input } from '../../components/ui/Controls/Input';
import { validateRequiredValue } from '../../helpers/validators';
type FormValues = { type FormValues = {
username: string; username: string;
password: string; password: string;
} };
type LoginFormProps = { type LoginFormProps = {
onSubmit: (data: FormValues) => void; onSubmit: (data: FormValues) => void;
processing: boolean; processing: boolean;
} };
const Form = ({ onSubmit, processing }: LoginFormProps) => { const Form = ({ onSubmit, processing }: LoginFormProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
register,
handleSubmit, handleSubmit,
formState: { errors, isValid }, control,
formState: { isValid },
} = useForm<FormValues>({ } = useForm<FormValues>({
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
@@ -30,58 +32,46 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
<form onSubmit={handleSubmit(onSubmit)} className="card"> <form onSubmit={handleSubmit(onSubmit)} className="card">
<div className="card-body p-6"> <div className="card-body p-6">
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
<label className="form__label" htmlFor="username"> <Controller
{t('username_label')} name="username"
</label> control={control}
rules={{ validate: validateRequiredValue }}
<input render={({ field, fieldState }) => (
id="username" <Input
type="text" {...field}
className="form-control" type="text"
placeholder={t('username_placeholder')} label={t('username_label')}
autoComplete="username" placeholder={t('username_placeholder')}
autoCapitalize="none" error={fieldState.error?.message}
disabled={processing} autoComplete="username"
{...register('username', { autoCapitalize="none"
required: t('form_error_required'), disabled={processing}
})} />
)}
/> />
{errors.username && (
<span className="form__message form__message--error">
{errors.username.message}
</span>
)}
</div> </div>
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
<label className="form__label" htmlFor="password"> <Controller
{t('password_label')} name="password"
</label> control={control}
rules={{ validate: validateRequiredValue }}
<input render={({ field, fieldState }) => (
id="password" <Input
type="password" {...field}
className="form-control" type="password"
placeholder={t('password_placeholder')} label={t('username_label')}
autoComplete="current-password" placeholder={t('password_placeholder')}
disabled={processing} error={fieldState.error?.message}
{...register('password', { autoComplete="current-password"
required: t('form_error_required'), disabled={processing}
})} />
)}
/> />
{errors.password && (
<span className="form__message form__message--error">
{errors.password.message}
</span>
)}
</div> </div>
<div className="form-footer"> <div className="form-footer">
<button <button type="submit" className="btn btn-success btn-block" disabled={processing || !isValid}>
type="submit"
className="btn btn-success btn-block"
disabled={processing || !isValid}
>
{t('sign_in')} {t('sign_in')}
</button> </button>
</div> </div>
@@ -90,4 +80,4 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
); );
}; };
export default Form; export default Form;