Improve UI forms

This commit is contained in:
IRBorisov 2023-12-08 00:33:47 +03:00
parent b4c1fd97fb
commit 286bb4f29d
11 changed files with 151 additions and 141 deletions

View File

@ -45,7 +45,7 @@ function Root() {
overflowY: showScroll ? 'scroll': 'auto' overflowY: showScroll ? 'scroll': 'auto'
}} }}
> >
<main className='w-full h-full min-w-fit' style={{minHeight: mainHeight}}> <main className='w-full h-full min-w-fit flex flex-col items-center' style={{minHeight: mainHeight}}>
<Outlet /> <Outlet />
</main> </main>

View File

@ -1,24 +0,0 @@
interface FormProps {
title?: string
className?: string
dimensions?: string
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
children: React.ReactNode
}
function Form({
title, className, onSubmit,
dimensions='max-w-xs',
children
}: FormProps) {
return (
<form
className={`border shadow-md py-2 clr-app px-6 flex flex-col gap-3 ${dimensions} ${className}`}
onSubmit={onSubmit}
>
{ title ? <h1 className='text-xl whitespace-nowrap'>{title}</h1> : null }
{children}
</form>);
}
export default Form;

View File

@ -1,4 +1,5 @@
interface OverlayProps { interface OverlayProps {
id?: string
children: React.ReactNode children: React.ReactNode
position?: string position?: string
className?: string className?: string
@ -6,13 +7,13 @@ interface OverlayProps {
} }
function Overlay({ function Overlay({
children, className, id, children, className,
position='top-0 right-0', position='top-0 right-0',
layer='z-pop' layer='z-pop'
}: OverlayProps) { }: OverlayProps) {
return ( return (
<div className='relative'> <div className='relative'>
<div className={`absolute ${className} ${position} ${layer}`}> <div id={id} className={`absolute ${className} ${position} ${layer}`}>
{children} {children}
</div> </div>
</div>); </div>);

View File

@ -36,7 +36,9 @@ function PDFViewer({
<Document <Document
file={file} file={file}
onLoadSuccess={onDocumentLoadSuccess} onLoadSuccess={onDocumentLoadSuccess}
className='mx-3 w-fit' className='px-3 w-fit'
loading='Загрузка PDF файла...'
error='Не удалось загрузить файл.'
> >
<Overlay position='top-6 left-1/2 -translate-x-1/2' className='flex select-none'> <Overlay position='top-6 left-1/2 -translate-x-1/2' className='flex select-none'>
<PageControls <PageControls

View File

@ -0,0 +1,33 @@
import { useAuth } from '../context/AuthContext';
import { useConceptNavigation } from '../context/NagivationContext';
import TextURL from './Common/TextURL';
function ExpectedAnonymous() {
const { user, logout } = useAuth();
const { navigateTo } = useConceptNavigation();
function logoutAndRedirect() {
logout(() => navigateTo('/login/'));
}
return (
<div className='flex flex-col items-center gap-3 py-6'>
<p className='font-semibold'>{`Вы вошли в систему как ${user?.username ?? ''}`}</p>
<div className='flex gap-3'>
<TextURL text='Новая схема' href='/rsform-create'/>
<span> | </span>
<TextURL text='Библиотека' href='/library'/>
<span> | </span>
<TextURL text='Справка' href='/manuals'/>
<span> | </span>
<span
className='cursor-pointer hover:underline text-url'
onClick={logoutAndRedirect}
>
Выйти
</span>
</div>
</div>);
}
export default ExpectedAnonymous;

View File

@ -5,7 +5,6 @@ import { toast } from 'react-toastify';
import BackendError from '../components/BackendError'; import BackendError from '../components/BackendError';
import Button from '../components/Common/Button'; import Button from '../components/Common/Button';
import Checkbox from '../components/Common/Checkbox'; import Checkbox from '../components/Common/Checkbox';
import Form from '../components/Common/Form';
import Label from '../components/Common/Label'; import Label from '../components/Common/Label';
import MiniButton from '../components/Common/MiniButton'; import MiniButton from '../components/Common/MiniButton';
import Overlay from '../components/Common/Overlay'; import Overlay from '../components/Common/Overlay';
@ -79,11 +78,11 @@ function CreateRSFormPage() {
return ( return (
<RequireAuth> <RequireAuth>
<div className='flex justify-center'> <form
<Form title='Создание концептуальной схемы' className='max-w-lg w-full py-3 px-6 flex flex-col gap-3'
onSubmit={handleSubmit} onSubmit={handleSubmit}
dimensions='max-w-lg w-full mt-4'
> >
<h1>Создание концептуальной схемы</h1>
<Overlay position='top-[-2.4rem] right-[-1rem]'> <Overlay position='top-[-2.4rem] right-[-1rem]'>
<input ref={inputRef} type='file' <input ref={inputRef} type='file'
style={{ display: 'none' }} style={{ display: 'none' }}
@ -123,7 +122,7 @@ function CreateRSFormPage() {
value={common} value={common}
setValue={value => setCommon(value ?? false)} setValue={value => setCommon(value ?? false)}
/> />
<div className='flex items-center justify-center gap-4 py-2'> <div className='flex items-center justify-around py-2'>
<SubmitButton <SubmitButton
text='Создать схему' text='Создать схему'
loading={processing} loading={processing}
@ -136,8 +135,7 @@ function CreateRSFormPage() {
/> />
</div> </div>
{error ? <BackendError error={error} /> : null} {error ? <BackendError error={error} /> : null}
</Form> </form>
</div>
</RequireAuth>); </RequireAuth>);
} }

View File

@ -3,14 +3,14 @@ import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import BackendError, { ErrorInfo } from '../components/BackendError'; import BackendError, { ErrorInfo } from '../components/BackendError';
import Form from '../components/Common/Form';
import SubmitButton from '../components/Common/SubmitButton'; import SubmitButton from '../components/Common/SubmitButton';
import TextInput from '../components/Common/TextInput'; import TextInput from '../components/Common/TextInput';
import TextURL from '../components/Common/TextURL'; import TextURL from '../components/Common/TextURL';
import ExpectedAnonymous from '../components/ExpectedAnonymous';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { useConceptNavigation } from '../context/NagivationContext'; import { useConceptNavigation } from '../context/NagivationContext';
import { useConceptTheme } from '../context/ThemeContext';
import { IUserLoginData } from '../models/library'; import { IUserLoginData } from '../models/library';
import { resources } from '../utils/constants';
function ProcessError({error}: {error: ErrorInfo}): React.ReactElement { function ProcessError({error}: {error: ErrorInfo}): React.ReactElement {
if (axios.isAxiosError(error) && error.response && error.response.status === 400) { if (axios.isAxiosError(error) && error.response && error.response.status === 400) {
@ -25,11 +25,10 @@ function ProcessError({error}: {error: ErrorInfo}): React.ReactElement {
} }
function LoginPage() { function LoginPage() {
const {mainHeight} = useConceptTheme();
const location = useLocation(); const location = useLocation();
const { navigateTo, navigateHistory } = useConceptNavigation(); const { navigateTo, navigateHistory } = useConceptNavigation();
const search = useLocation().search; const search = useLocation().search;
const { user, login, logout, loading, error, setError } = useAuth(); const { user, login, loading, error, setError } = useAuth();
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
@ -61,41 +60,17 @@ function LoginPage() {
} }
} }
function logoutAndRedirect() { if (user) {
logout(() => navigateTo('/login/')); return (<ExpectedAnonymous />);
} }
return ( return (
<div <form
className='flex items-start justify-center w-full pt-8 select-none' className='pt-12 pb-6 px-6 flex flex-col gap-3 w-[24rem]'
style={{minHeight: mainHeight}}
>
{user ?
<div className='flex flex-col items-center gap-2'>
<p className='font-semibold'>{`Вы вошли в систему как ${user.username}`}</p>
<p>
<TextURL text='Создать схему' href='/rsform-create'/>
<span> | </span>
<TextURL text='Библиотека' href='/library'/>
<span> | </span>
<TextURL text='Справка' href='/manuals'/>
<span> | </span>
<span
className='cursor-pointer hover:underline text-url'
onClick={logoutAndRedirect}
>
Выйти
</span>
</p>
</div> : null}
{!user ?
<Form
onSubmit={handleSubmit} onSubmit={handleSubmit}
dimensions='w-[24rem]'
> >
<img alt='Концепт Портал' <img alt='Концепт Портал'
src='/logo_full.svg' src={resources.logo}
className='max-h-[2.5rem] min-w-[2.5rem] mt-2 mb-4' className='max-h-[2.5rem] min-w-[2.5rem] mb-3'
/> />
<TextInput id='username' autoFocus required allowEnter <TextInput id='username' autoFocus required allowEnter
label='Имя пользователя' label='Имя пользователя'
@ -113,6 +88,7 @@ function LoginPage() {
text='Войти' text='Войти'
dimensions='w-[12rem]' dimensions='w-[12rem]'
loading={loading} loading={loading}
disabled={!username || !password}
/> />
</div> </div>
<div className='flex flex-col text-sm'> <div className='flex flex-col text-sm'>
@ -120,8 +96,7 @@ function LoginPage() {
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' /> <TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
</div> </div>
{error ? <ProcessError error={error} /> : null} {error ? <ProcessError error={error} /> : null}
</Form> : null} </form>);
</div>);
} }
export default LoginPage; export default LoginPage;

View File

@ -7,7 +7,7 @@ interface ViewTopicProps {
function ViewTopic({ topic }: ViewTopicProps) { function ViewTopic({ topic }: ViewTopicProps) {
return ( return (
<div className='w-full px-2 py-2'> <div className='w-full px-2 py-2 max-w-[80rem]'>
<InfoTopic topic={topic}/> <InfoTopic topic={topic}/>
</div>); </div>);
} }

View File

@ -2,7 +2,7 @@ import TextURL from '../components/Common/TextURL';
export function NotFoundPage() { export function NotFoundPage() {
return ( return (
<div className='flex flex-col px-4 py-2'> <div className='px-6 py-2'>
<h1 className='text-xl font-semibold'>Ошибка 404 - Страница не найдена</h1> <h1 className='text-xl font-semibold'>Ошибка 404 - Страница не найдена</h1>
<p className='mt-2'>Данная страница не существует или запрашиваемый объект отсутствует в базе данных</p> <p className='mt-2'>Данная страница не существует или запрашиваемый объект отсутствует в базе данных</p>
<TextURL href='/' text='Вернуться на Портал' /> <TextURL href='/' text='Вернуться на Портал' />

View File

@ -5,13 +5,17 @@ import { toast } from 'react-toastify';
import BackendError from '../components/BackendError'; import BackendError from '../components/BackendError';
import Button from '../components/Common/Button'; import Button from '../components/Common/Button';
import Checkbox from '../components/Common/Checkbox'; import Checkbox from '../components/Common/Checkbox';
import Form from '../components/Common/Form'; import ConceptTooltip from '../components/Common/ConceptTooltip';
import Overlay from '../components/Common/Overlay';
import SubmitButton from '../components/Common/SubmitButton'; import SubmitButton from '../components/Common/SubmitButton';
import TextInput from '../components/Common/TextInput'; import TextInput from '../components/Common/TextInput';
import TextURL from '../components/Common/TextURL'; import TextURL from '../components/Common/TextURL';
import ExpectedAnonymous from '../components/ExpectedAnonymous';
import { HelpIcon } from '../components/Icons';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { useConceptNavigation } from '../context/NagivationContext'; import { useConceptNavigation } from '../context/NagivationContext';
import { type IUserSignupData } from '../models/library'; import { type IUserSignupData } from '../models/library';
import { globalIDs } from '../utils/constants';
function RegisterPage() { function RegisterPage() {
const location = useLocation(); const location = useLocation();
@ -57,80 +61,99 @@ function RegisterPage() {
} }
} }
if (user) {
return (<ExpectedAnonymous />);
}
return ( return (
<div className='flex justify-center w-full py-2'> <form
{user ? <b>{`Вы вошли в систему как ${user.username}`}</b> : null} className='py-3 px-6 flex flex-col gap-3 h-fit'
{!user ? onSubmit={handleSubmit}
<Form >
title='Регистрация' <h1>Новый пользователь</h1>
onSubmit={handleSubmit} <div className='flex gap-12'>
dimensions='w-[30rem] mt-3' <div className='flex flex-col gap-3'>
> <div className='absolute'>
<TextInput id='username' required <Overlay
label='Имя пользователя' id={globalIDs.password_tooltip}
value={username} position='top-[4.8rem] left-[3.4rem] absolute'
onChange={event => setUsername(event.target.value)} >
/> <HelpIcon color='text-primary' size={5} />
<TextInput id='password' type='password' required </Overlay>
label='Пароль' <ConceptTooltip
value={password} anchorSelect={`#${globalIDs.password_tooltip}`}
onChange={event => setPassword(event.target.value)} offset={6}
/> >
<TextInput id='password2' required type='password' <p>- используйте уникальный пароль</p>
label='Повторите пароль' <p>- портал функционирует в тестовом режиме</p>
value={password2} </ConceptTooltip>
onChange={event => setPassword2(event.target.value)} </div>
/>
<div className='text-sm'> <TextInput id='username' required
<p>- используйте уникальный пароль</p> label='Имя пользователя (логин)'
<p>- портал функционирует в тестовом режиме</p> value={username}
{/* <p>- минимум 8 символов</p> dimensions='w-[12rem]'
<p>- большие, маленькие буквы, цифры</p> onChange={event => setUsername(event.target.value)}
<p>- минимум 1 спец. символ</p> */}
</div>
<TextInput id='email' required
label='email'
value={email}
onChange={event => setEmail(event.target.value)}
/>
<TextInput id='first_name'
label='Имя'
value={firstName}
onChange={event => setFirstName(event.target.value)}
/>
<TextInput id='last_name'
label='Фамилия'
value={lastName}
onChange={event => setLastName(event.target.value)}
/>
<div className='flex text-sm'>
<Checkbox
label='Принимаю условия'
value={acceptPrivacy}
setValue={setAcceptPrivacy}
/> />
<TextURL <TextInput id='password' type='password' required
text='обработки персональных данных...' label='Пароль'
href={'/manuals?topic=privacy'} dimensions='w-[15rem]'
value={password}
onChange={event => setPassword(event.target.value)}
/>
<TextInput id='password2' required type='password'
label='Повторите пароль'
dimensions='w-[15rem]'
value={password2}
onChange={event => setPassword2(event.target.value)}
/> />
</div> </div>
<div className='flex items-center justify-center w-full gap-4 my-4'> <div className='flex flex-col gap-3 w-[15rem]'>
<SubmitButton <TextInput id='email' required
text='Регистрировать' label='Электронная почта (email)'
dimensions='min-w-[10rem]' value={email}
loading={loading} onChange={event => setEmail(event.target.value)}
disabled={!acceptPrivacy}
/> />
<Button <TextInput id='first_name'
text='Отмена' label='Отображаемое имя'
dimensions='min-w-[10rem]' value={firstName}
onClick={() => handleCancel()} onChange={event => setFirstName(event.target.value)}
/>
<TextInput id='last_name'
label='Отображаемая фамилия'
value={lastName}
onChange={event => setLastName(event.target.value)}
/> />
</div> </div>
{error ? <BackendError error={error} /> : null} </div>
</Form> : null}
</div>); <div className='flex text-sm'>
<Checkbox
label='Принимаю условия'
value={acceptPrivacy}
setValue={setAcceptPrivacy}
/>
<TextURL
text='обработки персональных данных...'
href={'/manuals?topic=privacy'}
/>
</div>
<div className='flex items-center justify-around w-full my-3'>
<SubmitButton
text='Регистрировать'
dimensions='min-w-[10rem]'
loading={loading}
disabled={!acceptPrivacy}
/>
<Button
text='Назад'
dimensions='min-w-[10rem]'
onClick={() => handleCancel()}
/>
</div>
{error ? <BackendError error={error} /> : null}
</form>);
} }
export default RegisterPage; export default RegisterPage;

View File

@ -29,7 +29,8 @@ export const EXTEOR_TRS_FILE = '.trs';
*/ */
export const resources = { export const resources = {
graph_font: '/DejaVu.ttf', graph_font: '/DejaVu.ttf',
privacy_policy: '/privacy.pdf' privacy_policy: '/privacy.pdf',
logo: '/logo_full.svg'
}; };
/** /**
@ -59,6 +60,7 @@ export const urls = {
* Global unique IDs. * Global unique IDs.
*/ */
export const globalIDs = { export const globalIDs = {
password_tooltip: 'password-tooltip',
main_scroll: 'main-scroll', main_scroll: 'main-scroll',
library_item_editor: 'library-item-editor', library_item_editor: 'library-item-editor',
constituenta_editor: 'constituenta-editor' constituenta_editor: 'constituenta-editor'