Minor UI fixes

This commit is contained in:
IRBorisov 2023-08-27 16:35:17 +03:00
parent e8ec015633
commit 35754a59c7
22 changed files with 82 additions and 65 deletions

View File

@ -6,8 +6,8 @@ interface DropdownProps {
function Dropdown({ children, widthClass = 'w-fit', stretchLeft }: DropdownProps) { function Dropdown({ children, widthClass = 'w-fit', stretchLeft }: DropdownProps) {
return ( return (
<div className='relative'> <div className='relative text-sm'>
<div className={`absolute ${stretchLeft ? 'right-0' : 'left-0'} py-2 z-40 flex flex-col items-stretch justify-start px-2 mt-2 text-sm origin-top-right bg-white border border-gray-100 divide-y rounded-md shadow-lg dark:border-gray-500 dark:bg-gray-900 ${widthClass}`}> <div className={`absolute ${stretchLeft ? 'right-0' : 'left-0'} mt-2 z-40 flex flex-col items-stretch justify-start origin-top-right border divide-y rounded-md shadow-lg clr-input clr-border ${widthClass}`}>
{children} {children}
</div> </div>
</div> </div>

View File

@ -10,8 +10,8 @@ function Navigation () {
const navigate = useNavigate(); const navigate = useNavigate();
const { noNavigation, toggleNoNavigation } = useConceptTheme(); const { noNavigation, toggleNoNavigation } = useConceptTheme();
const navigateLibrary = () => { navigate('/library') }; const navigateLibrary = () => navigate('/library');
const navigateHelp = () => { navigate('/manuals') }; const navigateHelp = () => navigate('/manuals');
return ( return (
<nav className='sticky top-0 left-0 right-0 z-50 select-none h-fit'> <nav className='sticky top-0 left-0 right-0 z-50 select-none h-fit'>

View File

@ -16,14 +16,14 @@ function UserDropdown({ hideDropdown }: UserDropdownProps) {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const navigateProfile = () => { const navigateProfile = () => {
hideDropdown() hideDropdown();
navigate('/profile'); navigate('/profile');
}; };
const logoutAndRedirect = const logoutAndRedirect =
() => { () => {
hideDropdown(); hideDropdown();
logout(() => { navigate('/login/'); }) logout(() => navigate('/login/'));
}; };
const navigateMyWork = () => { const navigateMyWork = () => {
@ -33,10 +33,16 @@ function UserDropdown({ hideDropdown }: UserDropdownProps) {
return ( return (
<Dropdown widthClass='w-36' stretchLeft> <Dropdown widthClass='w-36' stretchLeft>
<DropdownButton description='Профиль пользователя' onClick={navigateProfile}> <DropdownButton
description='Профиль пользователя'
onClick={navigateProfile}
>
{user?.username} {user?.username}
</DropdownButton> </DropdownButton>
<DropdownButton description='Переключение темы оформления' onClick={toggleDarkMode}> <DropdownButton
description='Переключение темы оформления'
onClick={toggleDarkMode}
>
{darkMode ? 'Светлая тема' : 'Темная тема'} {darkMode ? 'Светлая тема' : 'Темная тема'}
</DropdownButton> </DropdownButton>
<DropdownButton onClick={navigateMyWork}> <DropdownButton onClick={navigateMyWork}>

View File

@ -30,7 +30,7 @@ function UserMenu() {
</div> </div>
{ user && menu.isActive && { user && menu.isActive &&
<UserDropdown <UserDropdown
hideDropdown={() => { menu.hide(); }} hideDropdown={() => menu.hide()}
/>} />}
</div> </div>
); );

View File

@ -283,9 +283,9 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => setError(error),
onSuccess: newData => { onSuccess: newData => reload(setProcessing, () => {
reload(setProcessing, () => { if (callback) callback(newData); }) if (callback) callback(newData);
} })
}); });
}, [setError, reload]); }, [setError, reload]);

View File

@ -60,7 +60,7 @@ function useCheckExpression({ schema }: { schema?: IRSForm }) {
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
const [parseData, setParseData] = useState<IExpressionParse | undefined>(undefined); const [parseData, setParseData] = useState<IExpressionParse | undefined>(undefined);
const resetParse = useCallback(() => { setParseData(undefined); }, []); const resetParse = useCallback(() => setParseData(undefined), []);
function checkExpression(expression: string, activeCst?: IConstituenta, onSuccess?: DataCallback<IExpressionParse>) { function checkExpression(expression: string, activeCst?: IConstituenta, onSuccess?: DataCallback<IExpressionParse>) {
setError(undefined); setError(undefined);

View File

@ -6,14 +6,14 @@ function useDropdown() {
const [isActive, setIsActive] = useState(false); const [isActive, setIsActive] = useState(false);
const ref = useRef(null); const ref = useRef(null);
useClickedOutside({ ref, callback: () => { setIsActive(false); } }) useClickedOutside({ ref, callback: () => setIsActive(false) })
return { return {
ref, ref,
isActive, isActive,
setIsActive, setIsActive,
toggle: () => { setIsActive(!isActive); }, toggle: () => setIsActive(!isActive),
hide: () => { setIsActive(false); } hide: () => setIsActive(false)
}; };
} }

View File

@ -27,7 +27,10 @@ export function useRSFormDetails({ target }: { target?: string }) {
getRSFormDetails(target, { getRSFormDetails(target, {
showError: true, showError: true,
setLoading: setCustomLoading ?? setLoading, setLoading: setCustomLoading ?? setLoading,
onError: error => { setInnerSchema(undefined); setError(error); }, onError: error => {
setInnerSchema(undefined);
setError(error);
},
onSuccess: schema => { onSuccess: schema => {
setSchema(schema); setSchema(schema);
if (callback) callback(); if (callback) callback();

View File

@ -9,7 +9,7 @@ function useResolveText({ schema }: { schema?: IRSForm }) {
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
const [refsData, setRefsData] = useState<IReferenceData | undefined>(undefined); const [refsData, setRefsData] = useState<IReferenceData | undefined>(undefined);
const resetData = useCallback(() => { setRefsData(undefined); }, []); const resetData = useCallback(() => setRefsData(undefined), []);
function resolveText(text: string, onSuccess?: DataCallback<IReferenceData>) { function resolveText(text: string, onSuccess?: DataCallback<IReferenceData>) {
setError(undefined); setError(undefined);
@ -17,7 +17,7 @@ function useResolveText({ schema }: { schema?: IRSForm }) {
data: { text: text }, data: { text: text },
showError: true, showError: true,
setLoading, setLoading,
onError: error => { setError(error); }, onError: error => setError(error),
onSuccess: data => { onSuccess: data => {
setRefsData(data); setRefsData(data);
if (onSuccess) onSuccess(data); if (onSuccess) onSuccess(data);

View File

@ -38,12 +38,14 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
<Checkbox <Checkbox
value={value === LibraryFilterStrategy.MANUAL} value={value === LibraryFilterStrategy.MANUAL}
label='Отображать все' label='Отображать все'
widthClass='w-fit px-2'
/> />
</DropdownButton> </DropdownButton>
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.COMMON)}> <DropdownButton onClick={() => handleChange(LibraryFilterStrategy.COMMON)}>
<Checkbox <Checkbox
value={value === LibraryFilterStrategy.COMMON} value={value === LibraryFilterStrategy.COMMON}
label='Общедоступные' label='Общедоступные'
widthClass='w-fit px-2'
tooltip='Отображать только общедоступные схемы' tooltip='Отображать только общедоступные схемы'
/> />
</DropdownButton> </DropdownButton>
@ -51,6 +53,7 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
<Checkbox <Checkbox
value={value === LibraryFilterStrategy.CANONICAL} value={value === LibraryFilterStrategy.CANONICAL}
label='Библиотечные' label='Библиотечные'
widthClass='w-fit px-2'
tooltip='Отображать только библиотечные схемы' tooltip='Отображать только библиотечные схемы'
/> />
</DropdownButton> </DropdownButton>
@ -58,6 +61,7 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
<Checkbox <Checkbox
value={value === LibraryFilterStrategy.PERSONAL} value={value === LibraryFilterStrategy.PERSONAL}
label='Личные' label='Личные'
widthClass='w-fit px-2'
tooltip='Отображать только подписки и владеемые схемы' tooltip='Отображать только подписки и владеемые схемы'
/> />
</DropdownButton> </DropdownButton>
@ -65,6 +69,7 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
<Checkbox <Checkbox
value={value === LibraryFilterStrategy.SUBSCRIBE} value={value === LibraryFilterStrategy.SUBSCRIBE}
label='Подписки' label='Подписки'
widthClass='w-fit px-2'
tooltip='Отображать только подписки' tooltip='Отображать только подписки'
/> />
</DropdownButton> </DropdownButton>
@ -72,6 +77,7 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
<Checkbox <Checkbox
value={value === LibraryFilterStrategy.OWNED} value={value === LibraryFilterStrategy.OWNED}
label='Я - Владелец!' label='Я - Владелец!'
widthClass='w-fit px-2'
tooltip='Отображать только владеемые схемы' tooltip='Отображать только владеемые схемы'
/> />
</DropdownButton> </DropdownButton>

View File

@ -34,7 +34,7 @@ function LoginPage() {
username: username, username: username,
password: password password: password
}; };
login(data, () => { navigate('/library?filter=personal'); }); login(data, () => navigate('/library'));
} }
} }
@ -54,14 +54,14 @@ function LoginPage() {
type='text' type='text'
value={username} value={username}
autoFocus autoFocus
onChange={event => { setUsername(event.target.value); }} onChange={event => setUsername(event.target.value)}
/> />
<TextInput id='password' <TextInput id='password'
label='Пароль' label='Пароль'
required required
type='password' type='password'
value={password} value={password}
onChange={event => { setPassword(event.target.value); }} onChange={event => setPassword(event.target.value)}
/> />
<div className='flex justify-center w-full gap-2 mt-4'> <div className='flex justify-center w-full gap-2 mt-4'>

View File

@ -65,21 +65,21 @@ function DlgCloneRSForm({ hideWindow }: DlgCloneRSFormProps) {
<TextInput id='title' label='Полное название' type='text' <TextInput id='title' label='Полное название' type='text'
required required
value={title} value={title}
onChange={event => { setTitle(event.target.value); }} onChange={event => setTitle(event.target.value)}
/> />
<TextInput id='alias' label='Сокращение' type='text' <TextInput id='alias' label='Сокращение' type='text'
required required
value={alias} value={alias}
widthClass='max-w-sm' widthClass='max-w-sm'
onChange={event => { setAlias(event.target.value); }} onChange={event => setAlias(event.target.value)}
/> />
<TextArea id='comment' label='Комментарий' <TextArea id='comment' label='Комментарий'
value={comment} value={comment}
onChange={event => { setComment(event.target.value); }} onChange={event => setComment(event.target.value)}
/> />
<Checkbox id='common' label='Общедоступная схема' <Checkbox id='common' label='Общедоступная схема'
value={common} value={common}
onChange={event => { setCommon(event.target.checked); }} onChange={event => setCommon(event.target.checked)}
/> />
</Modal> </Modal>
); );

View File

@ -34,9 +34,7 @@ function DlgCreateCst({ hideWindow, initial, onCreate }: DlgCreateCstProps) {
} }
} }
const handleSubmit = () => { const handleSubmit = () => onCreate(getData());
onCreate(getData());
};
useEffect(() => { useEffect(() => {
if (initial) { if (initial) {
@ -48,10 +46,7 @@ function DlgCreateCst({ hideWindow, initial, onCreate }: DlgCreateCstProps) {
} }
}, [initial]); }, [initial]);
useEffect(() => { useEffect(() => setValidated(selectedType !== undefined), [selectedType]);
setValidated(selectedType !== undefined);
}, [selectedType]
);
return ( return (
<Modal <Modal
@ -67,7 +62,7 @@ function DlgCreateCst({ hideWindow, initial, onCreate }: DlgCreateCstProps) {
options={CstTypeSelector} options={CstTypeSelector}
placeholder='Выберите тип' placeholder='Выберите тип'
values={selectedType ? [{ value: selectedType, label: getCstTypeLabel(selectedType) }] : []} values={selectedType ? [{ value: selectedType, label: getCstTypeLabel(selectedType) }] : []}
onChange={data => { setSelectedType(data.length > 0 ? data[0].value : CstType.BASE); }} onChange={data => setSelectedType(data.length > 0 ? data[0].value : CstType.BASE)}
/> />
</div> </div>
<TextArea id='term' label='Термин' <TextArea id='term' label='Термин'
@ -89,14 +84,14 @@ function DlgCreateCst({ hideWindow, initial, onCreate }: DlgCreateCstProps) {
rows={2} rows={2}
value={textDefinition} value={textDefinition}
spellCheck spellCheck
onChange={event => { setTextDefinition(event.target.value); }} onChange={event => setTextDefinition(event.target.value)}
/> />
<TextArea id='convention' label='Конвенция / Комментарий' <TextArea id='convention' label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию' placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию'
rows={2} rows={2}
value={convention} value={convention}
spellCheck spellCheck
onChange={event => { setConvention(event.target.value); }} onChange={event => setConvention(event.target.value)}
/> />
</div> </div>
</Modal> </Modal>

View File

@ -72,7 +72,7 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
options={CstTypeSelector} options={CstTypeSelector}
placeholder='Выберите тип' placeholder='Выберите тип'
values={cstType ? [{ value: cstType, label: getCstTypeLabel(cstType) }] : []} values={cstType ? [{ value: cstType, label: getCstTypeLabel(cstType) }] : []}
onChange={data => { setCstType(data.length > 0 ? data[0].value : CstType.BASE); }} onChange={data => setCstType(data.length > 0 ? data[0].value : CstType.BASE)}
/> />
<div> <div>
<TextInput id='alias' label='Имя' <TextInput id='alias' label='Имя'

View File

@ -32,7 +32,7 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
if (event.target.files && event.target.files.length > 0) { if (event.target.files && event.target.files.length > 0) {
setFile(event.target.files[0]); setFile(event.target.files[0]);
} else { } else {
setFile(undefined) setFile(undefined);
} }
} }
@ -53,7 +53,7 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
<Checkbox <Checkbox
label='Загружать название и комментарий' label='Загружать название и комментарий'
value={loadMetadata} value={loadMetadata}
onChange={event => { setLoadMetadata(event.target.checked); }} onChange={event => setLoadMetadata(event.target.checked)}
/> />
</div> </div>
</Modal> </Modal>

View File

@ -45,7 +45,8 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
const isEnabled = useMemo(() => activeCst && isEditable, [activeCst, isEditable]); const isEnabled = useMemo(() => activeCst && isEditable, [activeCst, isEditable]);
useLayoutEffect(() => { useLayoutEffect(
() => {
if (!activeCst) { if (!activeCst) {
setIsModified(false); setIsModified(false);
return; return;
@ -60,7 +61,8 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
activeCst?.definition.text.raw, activeCst?.convention, activeCst?.definition.text.raw, activeCst?.convention,
term, textDefinition, expression, convention]); term, textDefinition, expression, convention]);
useLayoutEffect(() => { useLayoutEffect(
() => {
if (activeCst) { if (activeCst) {
setAlias(activeCst.alias); setAlias(activeCst.alias);
setConvention(activeCst.convention ?? ''); setConvention(activeCst.convention ?? '');
@ -86,7 +88,7 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
definition_raw: textDefinition, definition_raw: textDefinition,
term_raw: term term_raw: term
}; };
cstUpdate(data, () => { toast.success('Изменения сохранены'); }); cstUpdate(data, () => toast.success('Изменения сохранены'));
} }
function handleDelete() { function handleDelete() {
@ -208,8 +210,8 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
resolved={activeCst?.definition.text.resolved ?? ''} resolved={activeCst?.definition.text.resolved ?? ''}
disabled={!isEnabled} disabled={!isEnabled}
spellCheck spellCheck
onChange={event => { setTextDefinition(event.target.value); }} onChange={event => setTextDefinition(event.target.value)}
onFocus={() => { setEditMode(EditMode.TEXT); }} onFocus={() => setEditMode(EditMode.TEXT)}
/> />
<TextArea id='convention' label='Конвенция / Комментарий' <TextArea id='convention' label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию' placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию'
@ -217,8 +219,8 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onRenameCst, onO
value={convention} value={convention}
disabled={!isEnabled} disabled={!isEnabled}
spellCheck spellCheck
onChange={event => { setConvention(event.target.value); }} onChange={event => setConvention(event.target.value)}
onFocus={() => { setEditMode(EditMode.TEXT); }} onFocus={() => setEditMode(EditMode.TEXT)}
/> />
<div className='flex justify-center w-full mt-4 mb-2'> <div className='flex justify-center w-full mt-4 mb-2'>
<SubmitButton <SubmitButton

View File

@ -53,7 +53,9 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
}, -1); }, -1);
const target = Math.max(0, currentIndex - 1) + 1 const target = Math.max(0, currentIndex - 1) + 1
const data = { const data = {
items: selected.map(id => { return { id: id }; }), items: selected.map(id => {
return { id: id };
}),
move_to: target move_to: target
} }
cstMoveTo(data); cstMoveTo(data);
@ -78,7 +80,9 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
}, -1); }, -1);
const target = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1 const target = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1
const data: ICstMovetoData = { const data: ICstMovetoData = {
items: selected.map(id => { return { id: id }; }), items: selected.map(id => {
return { id: id };
}),
move_to: target move_to: target
} }
cstMoveTo(data); cstMoveTo(data);
@ -89,7 +93,6 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
resetAliases(() => toast.success('Переиндексация конституент успешна')); resetAliases(() => toast.success('Переиндексация конституент успешна'));
} }
// Add new constituenta
function handleCreateCst(type?: CstType) { function handleCreateCst(type?: CstType) {
if (!schema) { if (!schema) {
return; return;

View File

@ -400,7 +400,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
searchable={false} searchable={false}
placeholder='Выберите цвет' placeholder='Выберите цвет'
values={coloringScheme ? [{ value: coloringScheme, label: mapColoringLabels.get(coloringScheme) }] : []} values={coloringScheme ? [{ value: coloringScheme, label: mapColoringLabels.get(coloringScheme) }] : []}
onChange={data => { setColoringScheme(data.length > 0 ? data[0].value : GraphColoringSelector[0].value); }} onChange={data => setColoringScheme(data.length > 0 ? data[0].value : GraphColoringSelector[0].value)}
/> />
</div> </div>
@ -410,7 +410,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
searchable={false} searchable={false}
placeholder='Способ расположения' placeholder='Способ расположения'
values={layout ? [{ value: layout, label: mapLayoutLabels.get(layout) }] : []} values={layout ? [{ value: layout, label: mapLayoutLabels.get(layout) }] : []}
onChange={data => { setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value); }} onChange={data => setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value)}
/> />
<Checkbox <Checkbox
label='Скрыть текст' label='Скрыть текст'

View File

@ -148,7 +148,9 @@ function RSTabs() {
return; return;
} }
const data = { const data = {
items: deleted.map(id => { return { id: id }; }) items: deleted.map(id => {
return { id: id };
})
}; };
let activeIndex = schema.items.findIndex(cst => cst.id === activeID); let activeIndex = schema.items.findIndex(cst => cst.id === activeID);
cstDelete(data, () => { cstDelete(data, () => {

View File

@ -12,7 +12,7 @@ function RSLocalButton({ text, tooltip, disabled, onInsert }: RSLocalButtonProps
<button <button
type='button' type='button'
disabled={disabled} disabled={disabled}
onClick={() => { onInsert(TokenID.ID_LOCAL, text); }} onClick={() => onInsert(TokenID.ID_LOCAL, text)}
title={tooltip} title={tooltip}
tabIndex={-1} tabIndex={-1}
className='w-[1.5rem] h-7 cursor-pointer border rounded-none clr-btn-clear' className='w-[1.5rem] h-7 cursor-pointer border rounded-none clr-btn-clear'

View File

@ -14,7 +14,7 @@ function RSTokenButton({ id, disabled, onInsert }: RSTokenButtonProps) {
<button <button
type='button' type='button'
disabled={disabled} disabled={disabled}
onClick={() => { onInsert(id); }} onClick={() => onInsert(id)}
title={data.tooltip} title={data.tooltip}
tabIndex={-1} tabIndex={-1}
className={`px-1 cursor-pointer border rounded-none h-7 ${width} clr-btn-clear`} className={`px-1 cursor-pointer border rounded-none h-7 ${width} clr-btn-clear`}

View File

@ -55,17 +55,17 @@ function RegisterPage() {
<TextInput id='username' label='Имя пользователя' type='text' <TextInput id='username' label='Имя пользователя' type='text'
required required
value={username} value={username}
onChange={event => { setUsername(event.target.value); }} onChange={event => setUsername(event.target.value)}
/> />
<TextInput id='password' label='Пароль' type='password' <TextInput id='password' label='Пароль' type='password'
required required
value={password} value={password}
onChange={event => { setPassword(event.target.value); }} onChange={event => setPassword(event.target.value)}
/> />
<TextInput id='password2' label='Повторите пароль' type='password' <TextInput id='password2' label='Повторите пароль' type='password'
required required
value={password2} value={password2}
onChange={event => { setPassword2(event.target.value); }} onChange={event => setPassword2(event.target.value)}
/> />
<div className='text-sm'> <div className='text-sm'>
<p>- используйте уникальный пароль</p> <p>- используйте уникальный пароль</p>
@ -78,15 +78,15 @@ function RegisterPage() {
<TextInput id='email' label='email' type='text' <TextInput id='email' label='email' type='text'
required required
value={email} value={email}
onChange={event => { setEmail(event.target.value); }} onChange={event => setEmail(event.target.value)}
/> />
<TextInput id='first_name' label='Имя' type='text' <TextInput id='first_name' label='Имя' type='text'
value={firstName} value={firstName}
onChange={event => { setFirstName(event.target.value); }} onChange={event => setFirstName(event.target.value)}
/> />
<TextInput id='last_name' label='Фамилия' type='text' <TextInput id='last_name' label='Фамилия' type='text'
value={lastName} value={lastName}
onChange={event => { setLastName(event.target.value); }} onChange={event => setLastName(event.target.value)}
/> />
<div className='flex items-center justify-center w-full my-4'> <div className='flex items-center justify-center w-full my-4'>