Improve navigation UI

This commit is contained in:
IRBorisov 2023-09-02 01:11:27 +03:00
parent 02021bd1d7
commit 2691860896
11 changed files with 149 additions and 91 deletions

View File

@ -7,7 +7,7 @@ interface DropdownProps {
function Dropdown({ children, widthClass = 'w-fit', stretchLeft }: DropdownProps) { function Dropdown({ children, widthClass = 'w-fit', stretchLeft }: DropdownProps) {
return ( return (
<div className='relative text-sm'> <div className='relative text-sm'>
<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}`}> <div className={`absolute ${stretchLeft ? 'right-0' : 'left-0'} mt-2 py-1 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

@ -1,16 +1,16 @@
interface NavigationTextItemProps { interface DropdownButtonProps {
description?: string | undefined tooltip?: string | undefined
onClick?: () => void onClick?: () => void
disabled?: boolean disabled?: boolean
children: React.ReactNode children: React.ReactNode
} }
function DropdownButton({ description = '', onClick, disabled, children }: NavigationTextItemProps) { function DropdownButton({ tooltip, onClick, disabled, children }: DropdownButtonProps) {
const behavior = (onClick ? 'cursor-pointer clr-hover' : 'cursor-default') + ' disabled:cursor-not-allowed'; const behavior = (onClick ? 'cursor-pointer disabled:cursor-not-allowed clr-hover' : 'cursor-default');
return ( return (
<button <button
disabled={disabled} disabled={disabled}
title={description} title={tooltip}
onClick={onClick} onClick={onClick}
className={`px-3 py-1 text-left overflow-ellipsis ${behavior} whitespace-nowrap`} className={`px-3 py-1 text-left overflow-ellipsis ${behavior} whitespace-nowrap`}
> >

View File

@ -0,0 +1,28 @@
import Checkbox from './Checkbox';
interface DropdownCheckboxProps {
label?: string
tooltip?: string
disabled?: boolean
value?: boolean
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
}
function DropdownCheckbox({ tooltip, onChange, disabled, ...props }: DropdownCheckboxProps) {
const behavior = (onChange && !disabled ? 'clr-hover' : '');
return (
<div
title={tooltip}
className={`px-4 py-1 text-left overflow-ellipsis ${behavior} whitespace-nowrap`}
>
<Checkbox
widthClass='w-fit'
disabled={disabled}
onChange={onChange}
{...props}
/>
</div>
);
}
export default DropdownCheckbox;

View File

@ -34,13 +34,13 @@ function UserDropdown({ hideDropdown }: UserDropdownProps) {
return ( return (
<Dropdown widthClass='w-36' stretchLeft> <Dropdown widthClass='w-36' stretchLeft>
<DropdownButton <DropdownButton
description='Профиль пользователя' tooltip='Профиль пользователя'
onClick={navigateProfile} onClick={navigateProfile}
> >
{user?.username} {user?.username}
</DropdownButton> </DropdownButton>
<DropdownButton <DropdownButton
description='Переключение темы оформления' tooltip='Переключение темы оформления'
onClick={toggleDarkMode} onClick={toggleDarkMode}
> >
{darkMode ? 'Светлая тема' : 'Темная тема'} {darkMode ? 'Светлая тема' : 'Темная тема'}

View File

@ -1,8 +1,9 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import BackendError from '../components/BackendError'; import BackendError from '../components/BackendError';
import Button from '../components/Common/Button';
import Checkbox from '../components/Common/Checkbox'; import Checkbox from '../components/Common/Checkbox';
import FileInput from '../components/Common/FileInput'; import FileInput from '../components/Common/FileInput';
import Form from '../components/Common/Form'; import Form from '../components/Common/Form';
@ -14,6 +15,7 @@ import { useLibrary } from '../context/LibraryContext';
import { IRSFormCreateData, LibraryItemType } from '../utils/models'; import { IRSFormCreateData, LibraryItemType } from '../utils/models';
function CreateRSFormPage() { function CreateRSFormPage() {
const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { createSchema, error, setError, processing } = useLibrary(); const { createSchema, error, setError, processing } = useLibrary();
@ -34,6 +36,14 @@ function CreateRSFormPage() {
setFile(undefined); setFile(undefined);
} }
} }
function handleCancel() {
if (location.key !== "default") {
navigate(-1);
} else {
navigate('/library');
}
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) { function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();
@ -89,10 +99,16 @@ function CreateRSFormPage() {
onChange={handleFile} onChange={handleFile}
/> />
<div className='flex items-center justify-center py-2 mt-4'> <div className='flex items-center justify-center gap-4 py-2 mt-4'>
<SubmitButton <SubmitButton
text='Создать схему' text='Создать схему'
loading={processing} loading={processing}
widthClass='min-w-[10rem]'
/>
<Button
text='Отмена'
onClick={() => handleCancel()}
widthClass='min-w-[10rem]'
/> />
</div> </div>
{ error && <BackendError error={error} />} { error && <BackendError error={error} />}

View File

@ -13,7 +13,7 @@ function HomePage() {
setTimeout(() => { setTimeout(() => {
navigate('/manuals'); navigate('/manuals');
}, TIMEOUT_UI_REFRESH); }, TIMEOUT_UI_REFRESH);
} else if(!user.is_staff) { } else {
setTimeout(() => { setTimeout(() => {
navigate('/library'); navigate('/library');
}, TIMEOUT_UI_REFRESH); }, TIMEOUT_UI_REFRESH);

View File

@ -1,9 +1,8 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import Button from '../../components/Common/Button'; import Button from '../../components/Common/Button';
import Checkbox from '../../components/Common/Checkbox';
import Dropdown from '../../components/Common/Dropdown'; import Dropdown from '../../components/Common/Dropdown';
import DropdownButton from '../../components/Common/DropdownButton'; import DropdownCheckbox from '../../components/Common/DropdownCheckbox';
import { FilterCogIcon } from '../../components/Icons'; import { FilterCogIcon } from '../../components/Icons';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import useDropdown from '../../hooks/useDropdown'; import useDropdown from '../../hooks/useDropdown';
@ -36,56 +35,44 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
/> />
{ pickerMenu.isActive && { pickerMenu.isActive &&
<Dropdown> <Dropdown>
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.MANUAL)}> <DropdownCheckbox
<Checkbox onChange={() => handleChange(LibraryFilterStrategy.MANUAL)}
value={value === LibraryFilterStrategy.MANUAL} value={value === LibraryFilterStrategy.MANUAL}
label='Отображать все' label='Отображать все'
widthClass='w-fit px-2' />
/> <DropdownCheckbox
</DropdownButton> onChange={() => handleChange(LibraryFilterStrategy.COMMON)}
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.COMMON)}> value={value === LibraryFilterStrategy.COMMON}
<Checkbox label='Общедоступные'
value={value === LibraryFilterStrategy.COMMON} tooltip='Отображать только общедоступные схемы'
label='Общедоступные' />
widthClass='w-fit px-2' <DropdownCheckbox
tooltip='Отображать только общедоступные схемы' onChange={() => handleChange(LibraryFilterStrategy.CANONICAL)}
/> value={value === LibraryFilterStrategy.CANONICAL}
</DropdownButton> label='Неизменные'
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.CANONICAL)}> tooltip='Отображать только неизменные схемы'
<Checkbox />
value={value === LibraryFilterStrategy.CANONICAL} <DropdownCheckbox
label='Неизменные' onChange={() => handleChange(LibraryFilterStrategy.PERSONAL)}
widthClass='w-fit px-2' value={value === LibraryFilterStrategy.PERSONAL}
tooltip='Отображать только неизменные схемы' label='Личные'
/> disabled={!user}
</DropdownButton> tooltip='Отображать только подписки и владеемые схемы'
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.PERSONAL)}> />
<Checkbox <DropdownCheckbox
value={value === LibraryFilterStrategy.PERSONAL} onChange={() => handleChange(LibraryFilterStrategy.SUBSCRIBE)}
label='Личные' value={value === LibraryFilterStrategy.SUBSCRIBE}
disabled={!user} label='Подписки'
widthClass='w-fit px-2' disabled={!user}
tooltip='Отображать только подписки и владеемые схемы' tooltip='Отображать только подписки'
/> />
</DropdownButton> <DropdownCheckbox
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.SUBSCRIBE)}> onChange={() => handleChange(LibraryFilterStrategy.OWNED)}
<Checkbox value={value === LibraryFilterStrategy.OWNED}
value={value === LibraryFilterStrategy.SUBSCRIBE} disabled={!user}
label='Подписки' label='Я - Владелец!'
disabled={!user} tooltip='Отображать только владеемые схемы'
widthClass='w-fit px-2' />
tooltip='Отображать только подписки'
/>
</DropdownButton>
<DropdownButton onClick={() => handleChange(LibraryFilterStrategy.OWNED)}>
<Checkbox
value={value === LibraryFilterStrategy.OWNED}
disabled={!user}
label='Я - Владелец!'
widthClass='w-fit px-2'
tooltip='Отображать только владеемые схемы'
/>
</DropdownButton>
</Dropdown>} </Dropdown>}
</div> </div>
); );

View File

@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { MagnifyingGlassIcon } from '../../components/Icons'; import { MagnifyingGlassIcon } from '../../components/Icons';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import useLocalStorage from '../../hooks/useLocalStorage';
import { ILibraryFilter, LibraryFilterStrategy } from '../../utils/models'; import { ILibraryFilter, LibraryFilterStrategy } from '../../utils/models';
import PickerStrategy from './PickerStrategy'; import PickerStrategy from './PickerStrategy';
@ -30,7 +31,7 @@ function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) {
const { user } = useAuth(); const { user } = useAuth();
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [strategy, setStrategy] = useState(LibraryFilterStrategy.MANUAL); const [strategy, setStrategy] = useLocalStorage<LibraryFilterStrategy>('search_strategy', LibraryFilterStrategy.MANUAL);
function handleChangeQuery(event: React.ChangeEvent<HTMLInputElement>) { function handleChangeQuery(event: React.ChangeEvent<HTMLInputElement>) {
const newQuery = event.target.value; const newQuery = event.target.value;
@ -49,11 +50,15 @@ function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) {
useLayoutEffect(() => { useLayoutEffect(() => {
const searchFilter = new URLSearchParams(search).get('filter') as LibraryFilterStrategy | null; const searchFilter = new URLSearchParams(search).get('filter') as LibraryFilterStrategy | null;
if (searchFilter === null) {
navigate(`/library?filter=${strategy}`);
return;
}
const inputStrategy = searchFilter && Object.values(LibraryFilterStrategy).includes(searchFilter) ? searchFilter : LibraryFilterStrategy.MANUAL; const inputStrategy = searchFilter && Object.values(LibraryFilterStrategy).includes(searchFilter) ? searchFilter : LibraryFilterStrategy.MANUAL;
setQuery('') setQuery('')
setStrategy(inputStrategy) setStrategy(inputStrategy)
setFilter(ApplyStrategy(inputStrategy)); setFilter(ApplyStrategy(inputStrategy));
}, [user, search, setQuery, setFilter]); }, [user, search, setQuery, setFilter, setStrategy, strategy, navigate]);
const handleChangeStrategy = useCallback( const handleChangeStrategy = useCallback(
(value: LibraryFilterStrategy) => { (value: LibraryFilterStrategy) => {

View File

@ -10,6 +10,7 @@ import { useAuth } from '../context/AuthContext';
import { IUserLoginData } from '../utils/models'; import { IUserLoginData } from '../utils/models';
function LoginPage() { function LoginPage() {
const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const search = useLocation().search; const search = useLocation().search;
const { user, login, loading, error, setError } = useAuth(); const { user, login, loading, error, setError } = useAuth();
@ -34,7 +35,13 @@ function LoginPage() {
username: username, username: username,
password: password password: password
}; };
login(data, () => navigate('/library')); login(data, () => {
if (location.key !== "default") {
navigate(-1);
} else {
navigate('/library');
}
});
} }
} }
@ -44,7 +51,7 @@ function LoginPage() {
? <b>{`Вы вошли в систему как ${user.username}`}</b> ? <b>{`Вы вошли в систему как ${user.username}`}</b>
: :
<Form <Form
title='Ввод данных пользователя' title='Вход в Портал'
onSubmit={handleSubmit} onSubmit={handleSubmit}
widthClass='w-[24rem]' widthClass='w-[24rem]'
> >
@ -64,10 +71,10 @@ function LoginPage() {
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 py-2 mt-4'>
<SubmitButton <SubmitButton
text='Вход' text='Вход'
widthClass='w-[7rem]' widthClass='w-[12rem]'
loading={loading} loading={loading}
/> />
</div> </div>

View File

@ -1,9 +1,9 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import Button from '../../components/Common/Button'; import Button from '../../components/Common/Button';
import Checkbox from '../../components/Common/Checkbox';
import Dropdown from '../../components/Common/Dropdown'; import Dropdown from '../../components/Common/Dropdown';
import DropdownButton from '../../components/Common/DropdownButton'; import DropdownButton from '../../components/Common/DropdownButton';
import DropdownCheckbox from '../../components/Common/DropdownCheckbox';
import { CloneIcon, CrownIcon, DownloadIcon, DumpBinIcon, EyeIcon, EyeOffIcon, MenuIcon, PenIcon, PlusIcon, ShareIcon, UploadIcon } from '../../components/Icons'; import { CloneIcon, CrownIcon, DownloadIcon, DumpBinIcon, EyeIcon, EyeOffIcon, MenuIcon, PenIcon, PlusIcon, ShareIcon, UploadIcon } from '../../components/Icons';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
@ -131,7 +131,7 @@ function RSTabsMenu({
<DropdownButton <DropdownButton
disabled={!user || !isClaimable} disabled={!user || !isClaimable}
onClick={!isOwned ? handleClaimOwner : undefined} onClick={!isOwned ? handleClaimOwner : undefined}
description={!user || !isClaimable ? 'Стать владельцем можно только для общей изменяемой схемы' : ''} tooltip={!user || !isClaimable ? 'Стать владельцем можно только для общей изменяемой схемы' : ''}
> >
<div className='inline-flex items-center gap-1 justify-normal'> <div className='inline-flex items-center gap-1 justify-normal'>
<span className={isOwned ? 'text-green' : ''}><CrownIcon size={4} /></span> <span className={isOwned ? 'text-green' : ''}><CrownIcon size={4} /></span>
@ -142,22 +142,18 @@ function RSTabsMenu({
</div> </div>
</DropdownButton> </DropdownButton>
{(isOwned || user?.is_staff) && {(isOwned || user?.is_staff) &&
<DropdownButton onClick={toggleReadonly}> <DropdownCheckbox
<Checkbox value={isReadonly}
value={isReadonly} onChange={toggleReadonly}
onChange={toggleReadonly} label='Я — читатель!'
label='Я — читатель!' tooltip='Режим чтения'
tooltip='Режим чтения' />}
/>
</DropdownButton>}
{user?.is_staff && {user?.is_staff &&
<DropdownButton onClick={toggleForceAdmin}> <DropdownCheckbox
<Checkbox value={isForceAdmin}
value={isForceAdmin} onChange={toggleForceAdmin}
onChange={toggleForceAdmin} label='режим администратора'
label='режим администратора' />}
/>
</DropdownButton>}
</Dropdown>} </Dropdown>}
</div> </div>
<div> <div>

View File

@ -1,8 +1,9 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import BackendError from '../components/BackendError'; import BackendError from '../components/BackendError';
import Button from '../components/Common/Button';
import Form from '../components/Common/Form'; 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';
@ -10,6 +11,7 @@ import { useAuth } from '../context/AuthContext';
import { type IUserSignupData } from '../utils/models'; import { type IUserSignupData } from '../utils/models';
function RegisterPage() { function RegisterPage() {
const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { user, signup, loading, error, setError } = useAuth(); const { user, signup, loading, error, setError } = useAuth();
@ -24,6 +26,14 @@ function RegisterPage() {
setError(undefined); setError(undefined);
}, [username, email, password, password2, setError]); }, [username, email, password, password2, setError]);
function handleCancel() {
if (location.key !== "default") {
navigate(-1);
} else {
navigate('/library');
}
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) { function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();
if (!loading) { if (!loading) {
@ -48,7 +58,7 @@ function RegisterPage() {
<b>{`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`}</b>} <b>{`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`}</b>}
{ !user && { !user &&
<Form <Form
title='Регистрация пользователя' title='Регистрация'
onSubmit={handleSubmit} onSubmit={handleSubmit}
widthClass='w-[24rem]' widthClass='w-[24rem]'
> >
@ -89,8 +99,17 @@ function RegisterPage() {
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 gap-4 my-4'>
<SubmitButton text='Регистрировать' loading={loading}/> <SubmitButton
text='Регистрировать'
loading={loading}
widthClass='min-w-[10rem]'
/>
<Button
text='Отмена'
onClick={() => handleCancel()}
widthClass='min-w-[10rem]'
/>
</div> </div>
{ error && <BackendError error={error} />} { error && <BackendError error={error} />}
</Form> </Form>