mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
Refactor navigation and library search
This commit is contained in:
parent
743ddf298e
commit
fc7af76cfe
|
@ -7,9 +7,8 @@ function Footer() {
|
||||||
<footer className='z-50 px-4 pt-2 pb-4 text-sm select-none whitespace-nowrap clr-footer'>
|
<footer className='z-50 px-4 pt-2 pb-4 text-sm select-none whitespace-nowrap clr-footer'>
|
||||||
<div className='flex items-stretch justify-center w-full gap-4 mx-auto'>
|
<div className='flex items-stretch justify-center w-full gap-4 mx-auto'>
|
||||||
<div className='underline'>
|
<div className='underline'>
|
||||||
|
<Link to='/library' tabIndex={-1}>Библиотека</Link> <br/>
|
||||||
<Link to='/manuals' tabIndex={-1}>Справка</Link> <br/>
|
<Link to='/manuals' tabIndex={-1}>Справка</Link> <br/>
|
||||||
<Link to='/library?filter=common' tabIndex={-1}>Библиотека</Link> <br/>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className=''>
|
<div className=''>
|
||||||
<a href={urls.concept} tabIndex={-1} className='underline'>Центр Концепт</a>
|
<a href={urls.concept} tabIndex={-1} className='underline'>Центр Концепт</a>
|
||||||
|
|
|
@ -1,12 +1,25 @@
|
||||||
|
import { EducationIcon, EyeIcon, GroupIcon } from '../Icons';
|
||||||
|
|
||||||
function HelpLibrary() {
|
function HelpLibrary() {
|
||||||
return (
|
return (
|
||||||
<div className=''>
|
<div className=''>
|
||||||
<h1>Библиотека концептуальных схем</h1>
|
<h1>Библиотека концептуальных схем</h1>
|
||||||
<p>В библиотеки собраны различные концептуальные схемы. Группировка и классификации схем на данный момент не проводится.</p>
|
<p>В библиотеки собраны различные концептуальные схемы.</p>
|
||||||
<p>На текущем этапе развития Портала происходит первичное наполнение Библиотеки концептуальными схемами.</p>
|
<p>Группировка и классификации схем на данный момент не проводится.</p>
|
||||||
<p>Доступен поиск схемы в Библиотеке с помощью инструментов, расположенных в верхней части страницы.</p>
|
<p>На текущем этапе происходит наполнение Библиотеки концептуальными схемами.</p>
|
||||||
<p>Аттрибут "общедоступная схема" делает схему видимой в разделе библиотека.</p>
|
<p>Поиск осуществлеяется с помощью инструментов в верхней части страницы.</p>
|
||||||
<p>Аттрибут "библиотечная схема" позволяет выделить неизменяемые схемы, используемые как примеры и основа последующих разработок.</p>
|
<div className='flex items-center gap-2'>
|
||||||
|
<EyeIcon size={4}/>
|
||||||
|
<p>Аттрибут <b>отслеживаемая</b> обозначает отслеживание схемы.</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<GroupIcon size={4}/>
|
||||||
|
<p>Аттрибут <b>общедоступная</b> делает схему видимой в разделе библиотека.</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<EducationIcon size={4}/>
|
||||||
|
<p>Аттрибут <b>библиотечная</b> выделяет неизменяемые стандартные схемы.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,8 @@ interface LogoProps {
|
||||||
function Logo({ title }: LogoProps) {
|
function Logo({ title }: LogoProps) {
|
||||||
return (
|
return (
|
||||||
<Link to='/' className='flex items-center mr-4' tabIndex={-1}>
|
<Link to='/' className='flex items-center mr-4' tabIndex={-1}>
|
||||||
<img src='/favicon.svg' className='min-h-[2.5rem] mr-2 min-w-[2.5rem]' alt=''/>
|
<img src='/favicon.svg' className='min-h-[2rem] mr-2 min-w-[2rem]' alt=''/>
|
||||||
<span className='self-center hidden text-2xl font-semibold lg:block whitespace-nowrap dark:text-white'>{title}</span>
|
<span className='self-center hidden text-xl font-semibold lg:block whitespace-nowrap dark:text-white'>{title}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,7 @@ import { useConceptTheme } from '../../context/ThemeContext';
|
||||||
import { EducationIcon, LibraryIcon } from '../Icons';
|
import { EducationIcon, LibraryIcon } from '../Icons';
|
||||||
import Logo from './Logo'
|
import Logo from './Logo'
|
||||||
import NavigationButton from './NavigationButton';
|
import NavigationButton from './NavigationButton';
|
||||||
import TopSearch from './TopSearch';
|
|
||||||
import UserMenu from './UserMenu';
|
import UserMenu from './UserMenu';
|
||||||
import UserTools from './UserTools';
|
|
||||||
|
|
||||||
function Navigation () {
|
function Navigation () {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -16,11 +14,11 @@ function Navigation () {
|
||||||
const navigateHelp = () => { navigate('/manuals') };
|
const navigateHelp = () => { navigate('/manuals') };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className='sticky top-0 left-0 right-0 z-50 h-fit'>
|
<nav className='sticky top-0 left-0 right-0 z-50 select-none h-fit'>
|
||||||
{!noNavigation &&
|
{!noNavigation &&
|
||||||
<button
|
<button
|
||||||
title='Скрыть навигацию'
|
title='Скрыть навигацию'
|
||||||
className='absolute top-0 right-0 z-[60] w-[1.2rem] h-[4rem] border-b-2 border-l-2 clr-nav rounded-none'
|
className='absolute top-0 right-0 z-[60] w-[1.2rem] border-b-2 border-l-2 clr-nav rounded-none'
|
||||||
onClick={toggleNoNavigation}
|
onClick={toggleNoNavigation}
|
||||||
>
|
>
|
||||||
<p>{'>'}</p><p>{'>'}</p>
|
<p>{'>'}</p><p>{'>'}</p>
|
||||||
|
@ -28,22 +26,30 @@ function Navigation () {
|
||||||
{noNavigation &&
|
{noNavigation &&
|
||||||
<button
|
<button
|
||||||
title='Показать навигацию'
|
title='Показать навигацию'
|
||||||
className='absolute top-0 right-0 z-[60] w-[4rem] h-[1.6rem] border-b-2 border-l-2 clr-nav rounded-none'
|
className='absolute top-0 right-0 z-[60] px-1 h-[1.6rem] border-b-2 border-l-2 clr-nav rounded-none'
|
||||||
onClick={toggleNoNavigation}
|
onClick={toggleNoNavigation}
|
||||||
>
|
>
|
||||||
{'∨∨∨'}
|
{'∨∨∨'}
|
||||||
</button>}
|
</button>}
|
||||||
{!noNavigation &&
|
{!noNavigation &&
|
||||||
<div className='pr-6 pl-2 py-2.5 h-[4rem] flex items-center justify-between border-b-2 clr-nav rounded-none'>
|
<div className='flex items-center justify-between py-1 pl-2 pr-6 border-b-2 rounded-none clr-nav'>
|
||||||
<div className='flex items-start justify-start select-none'>
|
<div className='flex items-center justify-start'>
|
||||||
<Logo title='КонцептПортал' />
|
<Logo title='КонцептПортал' />
|
||||||
<TopSearch />
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<UserTools/>
|
|
||||||
<div className='flex items-center pl-2'>
|
<div className='flex items-center pl-2'>
|
||||||
<NavigationButton icon={<LibraryIcon />} description='Общие схемы' onClick={navigateCommon} />
|
<NavigationButton
|
||||||
<NavigationButton icon={<EducationIcon />} description='Справка' onClick={navigateHelp} />
|
text='Библиотека'
|
||||||
|
description='Библиотека концептуальных схем'
|
||||||
|
icon={<LibraryIcon />}
|
||||||
|
onClick={navigateCommon}
|
||||||
|
/>
|
||||||
|
<NavigationButton
|
||||||
|
text='Справка'
|
||||||
|
description='Справочные материалы и обучение'
|
||||||
|
icon={<EducationIcon />}
|
||||||
|
onClick={navigateHelp}
|
||||||
|
/>
|
||||||
<UserMenu />
|
<UserMenu />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
interface NavigationButtonProps {
|
interface NavigationButtonProps {
|
||||||
id?: string
|
id?: string
|
||||||
|
text?: string
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode
|
||||||
description?: string
|
description?: string
|
||||||
colorClass?: string
|
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultColors = 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white'
|
function NavigationButton({ id, icon, description, onClick, text }: NavigationButtonProps) {
|
||||||
|
|
||||||
function NavigationButton({ id, icon, description, colorClass = defaultColors, onClick }: NavigationButtonProps) {
|
|
||||||
return (
|
return (
|
||||||
<button id={id}
|
<button id={id}
|
||||||
title={description}
|
title={description}
|
||||||
type='button'
|
type='button'
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={'min-w-fit p-2 mr-1 focus:ring-4 rounded-lg focus:ring-gray-300 dark:focus:ring-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 ' + colorClass}
|
className='flex gap-1 p-2 mr-1 rounded-lg min-w-fit whitespace-nowrap clr-btn-nav'
|
||||||
>
|
>
|
||||||
{icon}
|
{icon && <span>{icon}</span>}
|
||||||
|
{text && <span className='font-semibold'>{text}</span>}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { useNavSearch } from '../../context/NavSearchContext';
|
|
||||||
import { MagnifyingGlassIcon } from '../Icons';
|
|
||||||
|
|
||||||
function TopSearch() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { query, setQuery } = useNavSearch();
|
|
||||||
|
|
||||||
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
if (!url.href.includes('/library')) {
|
|
||||||
event.preventDefault();
|
|
||||||
navigate('/library?filter=query');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='hidden md:block md:pl-2'>
|
|
||||||
<div className='relative md:w-96'>
|
|
||||||
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
|
|
||||||
<MagnifyingGlassIcon />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
name='email'
|
|
||||||
id='topbar-search'
|
|
||||||
value={query}
|
|
||||||
className='text-sm block w-full pl-10 p-2.5 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-600 dark:border-gray-400 dark:placeholder-gray-200 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500'
|
|
||||||
placeholder='Поиск схемы...'
|
|
||||||
onChange={data => setQuery(data.target.value)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TopSearch;
|
|
|
@ -8,7 +8,7 @@ import UserDropdown from './UserDropdown';
|
||||||
|
|
||||||
function LoginRef() {
|
function LoginRef() {
|
||||||
return (
|
return (
|
||||||
<Link to='login' className='inline-block text-sm font-bold hover:underline'>
|
<Link to='login' className='inline-block h-full px-1 py-2 font-semibold rounded-lg hover:underline clr-btn-nav text-primary'>
|
||||||
Войти...
|
Войти...
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -19,6 +19,7 @@ function UserMenu() {
|
||||||
const menu = useDropdown();
|
const menu = useDropdown();
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref}>
|
<div ref={menu.ref}>
|
||||||
|
<div className='w-[4.2rem] flex justify-end'>
|
||||||
{ !user && <LoginRef />}
|
{ !user && <LoginRef />}
|
||||||
{ user &&
|
{ user &&
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
|
@ -26,6 +27,7 @@ function UserMenu() {
|
||||||
description={`Пользователь ${user?.username}`}
|
description={`Пользователь ${user?.username}`}
|
||||||
onClick={menu.toggle}
|
onClick={menu.toggle}
|
||||||
/>}
|
/>}
|
||||||
|
</div>
|
||||||
{ user && menu.isActive &&
|
{ user && menu.isActive &&
|
||||||
<UserDropdown
|
<UserDropdown
|
||||||
hideDropdown={() => { menu.hide(); }}
|
hideDropdown={() => { menu.hide(); }}
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { useAuth } from '../../context/AuthContext';
|
|
||||||
import ConceptTooltip from '../Common/ConceptTooltip';
|
|
||||||
import TextURL from '../Common/TextURL';
|
|
||||||
import { PlusIcon, SquaresIcon } from '../Icons';
|
|
||||||
import NavigationButton from './NavigationButton';
|
|
||||||
|
|
||||||
function UserTools() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { user } = useAuth();
|
|
||||||
|
|
||||||
const navigateCreateRSForm = () => navigate('/rsform-create');
|
|
||||||
const navigateMyWork = () => navigate('/library?filter=personal');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex items-center px-2 border-r-2 clr-border-nav'>
|
|
||||||
<span>
|
|
||||||
{ user &&
|
|
||||||
<NavigationButton
|
|
||||||
description='Новая схема'
|
|
||||||
icon={<PlusIcon />}
|
|
||||||
onClick={navigateCreateRSForm}
|
|
||||||
colorClass='text-url'
|
|
||||||
/>}
|
|
||||||
{ !user &&
|
|
||||||
<NavigationButton id='items-nav-help'
|
|
||||||
icon={<PlusIcon />}
|
|
||||||
/>}
|
|
||||||
<ConceptTooltip anchorSelect='#items-nav-help' clickable>
|
|
||||||
<div className='flex flex-col cursor-default'>
|
|
||||||
<p>Создание и редактирование концептуальных схем</p>
|
|
||||||
<p>доступно только <b>зарегистрированным пользователям</b></p>
|
|
||||||
<div className='flex flex-col self-center'>
|
|
||||||
<TextURL text='Войти в систему' href='/login'/>
|
|
||||||
<TextURL text='Зарегистрироваться' href='/signup'/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ConceptTooltip>
|
|
||||||
</span>
|
|
||||||
{ user && <NavigationButton icon={<SquaresIcon />} description='Мои схемы' onClick={navigateMyWork} /> }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UserTools;
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { createContext, useCallback, useContext, useState } from 'react';
|
|
||||||
|
|
||||||
interface INavSearchContext {
|
|
||||||
query: string
|
|
||||||
setQuery: (value: string) => void
|
|
||||||
resetQuery: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const NavSearchContext = createContext<INavSearchContext | null>(null);
|
|
||||||
export const useNavSearch = () => {
|
|
||||||
const context = useContext(NavSearchContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
'useNavSearch has to be used within <NavSearchState.Provider>'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NavSearchStateProps {
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NavSearchState = ({ children }: NavSearchStateProps) => {
|
|
||||||
const [query, setQuery] = useState('');
|
|
||||||
|
|
||||||
const resetQuery = useCallback(() => setQuery(''), []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NavSearchContext.Provider value={{
|
|
||||||
query,
|
|
||||||
setQuery,
|
|
||||||
resetQuery: resetQuery
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</NavSearchContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -94,7 +94,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
|
|
||||||
const isTracking = useMemo(
|
const isTracking = useMemo(
|
||||||
() => {
|
() => {
|
||||||
if (!schema || !user) {
|
if (!user || !schema || !user.id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return schema.subscribers.includes(user.id);
|
return schema.subscribers.includes(user.id);
|
||||||
|
@ -165,7 +165,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: error => setError(error),
|
onError: error => setError(error),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
if (!schema.subscribers.includes(user.id)) {
|
if (user.id && !schema.subscribers.includes(user.id)) {
|
||||||
schema.subscribers.push(user.id);
|
schema.subscribers.push(user.id);
|
||||||
}
|
}
|
||||||
if (!user.subscriptions.includes(schema.id)) {
|
if (!user.subscriptions.includes(schema.id)) {
|
||||||
|
@ -188,7 +188,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: error => setError(error),
|
onError: error => setError(error),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
if (schema.subscribers.includes(user.id)) {
|
if (user.id && schema.subscribers.includes(user.id)) {
|
||||||
schema.subscribers.splice(schema.subscribers.indexOf(user.id), 1);
|
schema.subscribers.splice(schema.subscribers.indexOf(user.id), 1);
|
||||||
}
|
}
|
||||||
if (user.subscriptions.includes(schema.id)) {
|
if (user.subscriptions.includes(schema.id)) {
|
||||||
|
|
|
@ -47,14 +47,14 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
|
||||||
const mainHeight = useMemo(
|
const mainHeight = useMemo(
|
||||||
() => {
|
() => {
|
||||||
return !noNavigation ?
|
return !noNavigation ?
|
||||||
'calc(100vh - 9.2rem)'
|
'calc(100vh - 8.6rem)'
|
||||||
: '100vh';
|
: '100vh';
|
||||||
}, [noNavigation]);
|
}, [noNavigation]);
|
||||||
|
|
||||||
const viewportHeight = useMemo(
|
const viewportHeight = useMemo(
|
||||||
() => {
|
() => {
|
||||||
return !noNavigation ?
|
return !noNavigation ?
|
||||||
'calc(100vh - 4.5rem)'
|
'calc(100vh - 3.9rem)'
|
||||||
: '100vh';
|
: '100vh';
|
||||||
}, [noNavigation]);
|
}, [noNavigation]);
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,10 @@
|
||||||
@apply hover:bg-gray-200 hover:text-gray-700 dark:hover:text-white dark:hover:bg-gray-500
|
@apply hover:bg-gray-200 hover:text-gray-700 dark:hover:text-white dark:hover:bg-gray-500
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clr-btn-nav {
|
||||||
|
@apply text-gray-500 hover:text-gray-900 dark:text-gray-200 dark:hover:text-white focus:ring-gray-300 dark:focus:ring-gray-400 hover:bg-gray-100 dark:hover:bg-gray-600
|
||||||
|
}
|
||||||
|
|
||||||
.clr-btn-primary {
|
.clr-btn-primary {
|
||||||
@apply text-white bg-blue-400 hover:bg-blue-600 dark:bg-orange-600 dark:hover:bg-orange-400 disabled:bg-gray-400 dark:disabled:bg-gray-600
|
@apply text-white bg-blue-400 hover:bg-blue-600 dark:bg-orange-600 dark:hover:bg-orange-400 disabled:bg-gray-400 dark:disabled:bg-gray-600
|
||||||
}
|
}
|
||||||
|
@ -115,7 +119,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-url {
|
.text-url {
|
||||||
@apply hover:text-blue-600 text-blue-400 dark:text-orange-600 dark:hover:text-orange-400
|
@apply hover:text-blue-600 text-blue-400 dark:text-orange-500 dark:hover:text-orange-300
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-red {
|
.text-red {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import App from './App.tsx'
|
||||||
import ErrorFallback from './components/ErrorFallback.tsx';
|
import ErrorFallback from './components/ErrorFallback.tsx';
|
||||||
import { AuthState } from './context/AuthContext.tsx';
|
import { AuthState } from './context/AuthContext.tsx';
|
||||||
import { LibraryState } from './context/LibraryContext.tsx';
|
import { LibraryState } from './context/LibraryContext.tsx';
|
||||||
import { NavSearchState } from './context/NavSearchContext.tsx';
|
|
||||||
import { ThemeState } from './context/ThemeContext.tsx';
|
import { ThemeState } from './context/ThemeContext.tsx';
|
||||||
import { UsersState } from './context/UsersContext.tsx';
|
import { UsersState } from './context/UsersContext.tsx';
|
||||||
import { initBackend } from './utils/backendAPI.ts';
|
import { initBackend } from './utils/backendAPI.ts';
|
||||||
|
@ -36,7 +35,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
>
|
>
|
||||||
<IntlProvider locale='ru' defaultLocale='ru'>
|
<IntlProvider locale='ru' defaultLocale='ru'>
|
||||||
<ThemeState>
|
<ThemeState>
|
||||||
<NavSearchState>
|
|
||||||
<AuthState>
|
<AuthState>
|
||||||
<UsersState>
|
<UsersState>
|
||||||
<LibraryState>
|
<LibraryState>
|
||||||
|
@ -46,7 +44,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
</LibraryState>
|
</LibraryState>
|
||||||
</UsersState>
|
</UsersState>
|
||||||
</AuthState>
|
</AuthState>
|
||||||
</NavSearchState>
|
|
||||||
</ThemeState>
|
</ThemeState>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
70
rsconcept/frontend/src/pages/LibraryPage/SearchPanel.tsx
Normal file
70
rsconcept/frontend/src/pages/LibraryPage/SearchPanel.tsx
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { useLayoutEffect, useState } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { MagnifyingGlassIcon } from '../../components/Icons';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
|
import { ILibraryFilter } from '../../utils/models';
|
||||||
|
|
||||||
|
interface SearchPanelProps {
|
||||||
|
filter: ILibraryFilter
|
||||||
|
setFilter: React.Dispatch<React.SetStateAction<ILibraryFilter>>
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchPanel({ filter, setFilter }: SearchPanelProps) {
|
||||||
|
const search = useLocation().search;
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const filterType = new URLSearchParams(search).get('filter');
|
||||||
|
if (filterType === 'common') {
|
||||||
|
setQuery('');
|
||||||
|
setFilter({
|
||||||
|
is_common: true
|
||||||
|
});
|
||||||
|
} else if (filterType === 'personal' && user) {
|
||||||
|
setQuery('');
|
||||||
|
setFilter({
|
||||||
|
ownedBy: user.id!
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [user, search, setQuery, setFilter]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='sticky top-0 left-0 right-0 z-10 flex justify-center w-full border-b clr-bg-pop'>
|
||||||
|
<div className='relative w-96'>
|
||||||
|
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
|
||||||
|
<MagnifyingGlassIcon />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={query}
|
||||||
|
className='w-full p-2 pl-10 text-sm outline-none clr-bg-pop border-x clr-border'
|
||||||
|
placeholder='Поиск схемы...'
|
||||||
|
onChange={data => setQuery(data.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchPanel;
|
||||||
|
|
||||||
|
|
||||||
|
{/* <div className='sticky top-0 left-0 right-0 z-10 flex items-start justify-between w-full gap-1 px-2 py-1 bg-white border-b rounded clr-bg-pop clr-border'>
|
||||||
|
<MatchModePicker
|
||||||
|
value={filterMatch}
|
||||||
|
onChange={setFilterMatch}
|
||||||
|
/>
|
||||||
|
<input type='text'
|
||||||
|
className='w-full px-2 bg-white outline-none select-none hover:text-clip clr-bg-pop'
|
||||||
|
placeholder='наберите текст фильтра'
|
||||||
|
value={filterText}
|
||||||
|
onChange={event => setFilterText(event.target.value)}
|
||||||
|
/>
|
||||||
|
<DependencyModePicker
|
||||||
|
value={filterSource}
|
||||||
|
onChange={setFilterSource}
|
||||||
|
/>
|
||||||
|
</div> */}
|
|
@ -3,20 +3,22 @@ import { useIntl } from 'react-intl';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import ConceptDataTable from '../../components/Common/ConceptDataTable';
|
import ConceptDataTable from '../../components/Common/ConceptDataTable';
|
||||||
|
import ConceptTooltip from '../../components/Common/ConceptTooltip';
|
||||||
|
import MiniButton from '../../components/Common/MiniButton';
|
||||||
import TextURL from '../../components/Common/TextURL';
|
import TextURL from '../../components/Common/TextURL';
|
||||||
import { EducationIcon, EyeIcon, GroupIcon } from '../../components/Icons';
|
import HelpLibrary from '../../components/Help/HelpLibrary';
|
||||||
|
import { EducationIcon, EyeIcon, GroupIcon, HelpIcon, PlusIcon } from '../../components/Icons';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { useNavSearch } from '../../context/NavSearchContext';
|
|
||||||
import { useUsers } from '../../context/UsersContext';
|
import { useUsers } from '../../context/UsersContext';
|
||||||
import { prefixes } from '../../utils/constants';
|
import { prefixes } from '../../utils/constants';
|
||||||
import { ILibraryItem } from '../../utils/models'
|
import { ILibraryItem } from '../../utils/models';
|
||||||
|
|
||||||
interface ViewLibraryProps {
|
interface ViewLibraryProps {
|
||||||
items: ILibraryItem[]
|
items: ILibraryItem[]
|
||||||
|
cleanQuery: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ViewLibrary({ items }: ViewLibraryProps) {
|
function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {
|
||||||
const { resetQuery: cleanQuery } = useNavSearch();
|
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
@ -24,6 +26,10 @@ function ViewLibrary({ items }: ViewLibraryProps) {
|
||||||
|
|
||||||
const openRSForm = (item: ILibraryItem) => navigate(`/rsforms/${item.id}`);
|
const openRSForm = (item: ILibraryItem) => navigate(`/rsforms/${item.id}`);
|
||||||
|
|
||||||
|
function handleCreateNew() {
|
||||||
|
navigate('/rsform-create');
|
||||||
|
}
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
|
@ -83,6 +89,26 @@ function ViewLibrary({ items }: ViewLibraryProps) {
|
||||||
], [intl, getUserLabel, user]);
|
], [intl, getUserLabel, user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='relative w-full'>
|
||||||
|
<div className='absolute top-0 left-0 z-20 flex gap-1 mt-2 ml-1'>
|
||||||
|
<MiniButton
|
||||||
|
onClick={handleCreateNew}
|
||||||
|
tooltip='Создать схему'
|
||||||
|
noHover
|
||||||
|
disabled={!user || !user.id}
|
||||||
|
icon={<PlusIcon color={!user || !user.id ? '' : 'text-primary'} size={5} />}
|
||||||
|
/>
|
||||||
|
<div id='library-help' className='py-2'>
|
||||||
|
<HelpIcon color='text-primary' size={5} />
|
||||||
|
</div>
|
||||||
|
<ConceptTooltip anchorSelect='#library-help'>
|
||||||
|
<div className='max-w-[35rem]'>
|
||||||
|
<HelpLibrary />
|
||||||
|
</div>
|
||||||
|
</ConceptTooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<ConceptDataTable
|
<ConceptDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={items}
|
data={items}
|
||||||
|
@ -111,6 +137,7 @@ function ViewLibrary({ items }: ViewLibraryProps) {
|
||||||
paginationRowsPerPageOptions={[10, 20, 30, 50, 100]}
|
paginationRowsPerPageOptions={[10, 20, 30, 50, 100]}
|
||||||
onRowClicked={openRSForm}
|
onRowClicked={openRSForm}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,49 +1,39 @@
|
||||||
import { useLayoutEffect, useState } from 'react';
|
import { useLayoutEffect, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
import BackendError from '../../components/BackendError'
|
import BackendError from '../../components/BackendError'
|
||||||
import { Loader } from '../../components/Common/Loader'
|
import { Loader } from '../../components/Common/Loader'
|
||||||
import { useAuth } from '../../context/AuthContext';
|
|
||||||
import { useLibrary } from '../../context/LibraryContext';
|
import { useLibrary } from '../../context/LibraryContext';
|
||||||
import { useNavSearch } from '../../context/NavSearchContext';
|
|
||||||
import { ILibraryFilter, ILibraryItem } from '../../utils/models';
|
import { ILibraryFilter, ILibraryItem } from '../../utils/models';
|
||||||
|
import SearchPanel from './SearchPanel';
|
||||||
import ViewLibrary from './ViewLibrary';
|
import ViewLibrary from './ViewLibrary';
|
||||||
|
|
||||||
function LibraryPage() {
|
function LibraryPage() {
|
||||||
const search = useLocation().search;
|
|
||||||
const { query, resetQuery: cleanQuery } = useNavSearch();
|
|
||||||
const { user } = useAuth();
|
|
||||||
const library = useLibrary();
|
const library = useLibrary();
|
||||||
|
|
||||||
const [ filterParams, setFilterParams ] = useState<ILibraryFilter>({});
|
const [ filterParams, setFilterParams ] = useState<ILibraryFilter>({});
|
||||||
const [ items, setItems ] = useState<ILibraryItem[]>([]);
|
const [ items, setItems ] = useState<ILibraryItem[]>([]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const filterType = new URLSearchParams(search).get('filter');
|
|
||||||
if (filterType === 'common') {
|
|
||||||
cleanQuery();
|
|
||||||
setFilterParams({
|
|
||||||
is_common: true
|
|
||||||
});
|
|
||||||
} else if (filterType === 'personal' && user) {
|
|
||||||
cleanQuery();
|
|
||||||
setFilterParams({
|
|
||||||
ownedBy: user.id!
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [user, search, cleanQuery]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const filter = filterParams;
|
const filter = filterParams;
|
||||||
filterParams.queryMeta = query ? query: undefined;
|
|
||||||
setItems(library.filter(filter));
|
setItems(library.filter(filter));
|
||||||
}, [query, library, filterParams]);
|
}, [library, filterParams]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
{ library.loading && <Loader /> }
|
{ library.loading && <Loader /> }
|
||||||
{ library.error && <BackendError error={library.error} />}
|
{ library.error && <BackendError error={library.error} />}
|
||||||
{ !library.loading && library.items && <ViewLibrary items={items} /> }
|
{ !library.loading && library.items &&
|
||||||
|
<div className='flex flex-col w-full'>
|
||||||
|
<SearchPanel
|
||||||
|
filter={filterParams}
|
||||||
|
setFilter={setFilterParams}
|
||||||
|
/>
|
||||||
|
<ViewLibrary
|
||||||
|
cleanQuery={() => setFilterParams({})}
|
||||||
|
items={items}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -337,8 +337,8 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
|
||||||
const canvasHeight = useMemo(
|
const canvasHeight = useMemo(
|
||||||
() => {
|
() => {
|
||||||
return !noNavigation ?
|
return !noNavigation ?
|
||||||
'calc(100vh - 10.5rem)'
|
'calc(100vh - 9.8rem)'
|
||||||
: 'calc(100vh - 2rem)';
|
: 'calc(100vh - 1.8rem)';
|
||||||
}, [noNavigation]);
|
}, [noNavigation]);
|
||||||
|
|
||||||
const dismissedStyle = useCallback(
|
const dismissedStyle = useCallback(
|
||||||
|
|
Loading…
Reference in New Issue
Block a user