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) {
return (
<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}
</div>
</div>

View File

@ -1,16 +1,16 @@
interface NavigationTextItemProps {
description?: string | undefined
interface DropdownButtonProps {
tooltip?: string | undefined
onClick?: () => void
disabled?: boolean
children: React.ReactNode
}
function DropdownButton({ description = '', onClick, disabled, children }: NavigationTextItemProps) {
const behavior = (onClick ? 'cursor-pointer clr-hover' : 'cursor-default') + ' disabled:cursor-not-allowed';
function DropdownButton({ tooltip, onClick, disabled, children }: DropdownButtonProps) {
const behavior = (onClick ? 'cursor-pointer disabled:cursor-not-allowed clr-hover' : 'cursor-default');
return (
<button
disabled={disabled}
title={description}
title={tooltip}
onClick={onClick}
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 (
<Dropdown widthClass='w-36' stretchLeft>
<DropdownButton
description='Профиль пользователя'
tooltip='Профиль пользователя'
onClick={navigateProfile}
>
{user?.username}
</DropdownButton>
<DropdownButton
description='Переключение темы оформления'
tooltip='Переключение темы оформления'
onClick={toggleDarkMode}
>
{darkMode ? 'Светлая тема' : 'Темная тема'}

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { MagnifyingGlassIcon } from '../../components/Icons';
import { useAuth } from '../../context/AuthContext';
import useLocalStorage from '../../hooks/useLocalStorage';
import { ILibraryFilter, LibraryFilterStrategy } from '../../utils/models';
import PickerStrategy from './PickerStrategy';
@ -30,7 +31,7 @@ function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) {
const { user } = useAuth();
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>) {
const newQuery = event.target.value;
@ -49,11 +50,15 @@ function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) {
useLayoutEffect(() => {
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;
setQuery('')
setStrategy(inputStrategy)
setFilter(ApplyStrategy(inputStrategy));
}, [user, search, setQuery, setFilter]);
}, [user, search, setQuery, setFilter, setStrategy, strategy, navigate]);
const handleChangeStrategy = useCallback(
(value: LibraryFilterStrategy) => {

View File

@ -10,6 +10,7 @@ import { useAuth } from '../context/AuthContext';
import { IUserLoginData } from '../utils/models';
function LoginPage() {
const location = useLocation();
const navigate = useNavigate();
const search = useLocation().search;
const { user, login, loading, error, setError } = useAuth();
@ -34,7 +35,13 @@ function LoginPage() {
username: username,
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>
:
<Form
title='Ввод данных пользователя'
title='Вход в Портал'
onSubmit={handleSubmit}
widthClass='w-[24rem]'
>
@ -64,10 +71,10 @@ function LoginPage() {
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
text='Вход'
widthClass='w-[7rem]'
widthClass='w-[12rem]'
loading={loading}
/>
</div>

View File

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

View File

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