mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Refactoring: rework application files structure
This commit is contained in:
parent
5310e0c9ed
commit
fa6fef5fa5
|
@ -1,102 +0,0 @@
|
|||
import { createBrowserRouter, Outlet, RouterProvider } from 'react-router-dom';
|
||||
|
||||
import ConceptToaster from '@/components/ConceptToaster';
|
||||
import Footer from '@/components/Footer';
|
||||
import Navigation from '@/components/Navigation';
|
||||
import { NavigationState } from '@/context/NavigationContext';
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import CreateRSFormPage from '@/pages/CreateRSFormPage';
|
||||
import HomePage from '@/pages/HomePage';
|
||||
import LibraryPage from '@/pages/LibraryPage';
|
||||
import LoginPage from '@/pages/LoginPage';
|
||||
import ManualsPage from '@/pages/ManualsPage';
|
||||
import NotFoundPage from '@/pages/NotFoundPage';
|
||||
import RegisterPage from '@/pages/RegisterPage';
|
||||
import RestorePasswordPage from '@/pages/RestorePasswordPage';
|
||||
import RSFormPage from '@/pages/RSFormPage';
|
||||
import UserProfilePage from '@/pages/UserProfilePage';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
function Root() {
|
||||
const { viewportHeight, mainHeight, showScroll } = useConceptTheme();
|
||||
return (
|
||||
<NavigationState>
|
||||
<div className='min-w-[30rem] clr-app antialiased'>
|
||||
<ConceptToaster
|
||||
className='mt-[4rem] text-sm' // prettier: split lines
|
||||
autoClose={3000}
|
||||
draggable={false}
|
||||
pauseOnFocusLoss={false}
|
||||
/>
|
||||
|
||||
<Navigation />
|
||||
|
||||
<div
|
||||
id={globalIDs.main_scroll}
|
||||
className='overflow-y-auto overscroll-none min-w-fit'
|
||||
style={{
|
||||
maxHeight: viewportHeight,
|
||||
overflowY: showScroll ? 'scroll' : 'auto'
|
||||
}}
|
||||
>
|
||||
<main className='flex flex-col items-center' style={{ minHeight: mainHeight }}>
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</NavigationState>
|
||||
);
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <Root />,
|
||||
errorElement: <NotFoundPage />,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <HomePage />
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
element: <LoginPage />
|
||||
},
|
||||
{
|
||||
path: 'signup',
|
||||
element: <RegisterPage />
|
||||
},
|
||||
{
|
||||
path: 'restore-password',
|
||||
element: <RestorePasswordPage />
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
element: <UserProfilePage />
|
||||
},
|
||||
{
|
||||
path: 'manuals',
|
||||
element: <ManualsPage />
|
||||
},
|
||||
{
|
||||
path: 'library',
|
||||
element: <LibraryPage />
|
||||
},
|
||||
{
|
||||
path: 'library/create',
|
||||
element: <CreateRSFormPage />
|
||||
},
|
||||
{
|
||||
path: 'rsforms/:id',
|
||||
element: <RSFormPage />
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
export default App;
|
42
rsconcept/frontend/src/app/ApplicationLayout.tsx
Normal file
42
rsconcept/frontend/src/app/ApplicationLayout.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import ConceptToaster from '@/app/ConceptToaster';
|
||||
import Footer from '@/app/Footer';
|
||||
import Navigation from '@/app/Navigation';
|
||||
import { NavigationState } from '@/context/NavigationContext';
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
function ApplicationLayout() {
|
||||
const { viewportHeight, mainHeight, showScroll } = useConceptTheme();
|
||||
return (
|
||||
<NavigationState>
|
||||
<div className='min-w-[30rem] clr-app antialiased'>
|
||||
<ConceptToaster
|
||||
className='mt-[4rem] text-sm' // prettier: split lines
|
||||
autoClose={3000}
|
||||
draggable={false}
|
||||
pauseOnFocusLoss={false}
|
||||
/>
|
||||
|
||||
<Navigation />
|
||||
|
||||
<div
|
||||
id={globalIDs.main_scroll}
|
||||
className='overflow-y-auto overscroll-none min-w-fit'
|
||||
style={{
|
||||
maxHeight: viewportHeight,
|
||||
overflowY: showScroll ? 'scroll' : 'auto'
|
||||
}}
|
||||
>
|
||||
<main className='flex flex-col items-center' style={{ minHeight: mainHeight }}>
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</NavigationState>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApplicationLayout;
|
|
@ -1,7 +1,7 @@
|
|||
import { type FallbackProps } from 'react-error-boundary';
|
||||
|
||||
import Button from './Common/Button';
|
||||
import InfoError from './InfoError';
|
||||
import Button from '../components/Common/Button';
|
||||
import InfoError from '../components/InfoError';
|
||||
|
||||
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||
return (
|
|
@ -3,7 +3,7 @@ import clsx from 'clsx';
|
|||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import { urls } from '@/utils/constants';
|
||||
|
||||
import TextURL from './Common/TextURL';
|
||||
import TextURL from '../components/Common/TextURL';
|
||||
|
||||
function Footer() {
|
||||
const { noNavigation, noFooter } = useConceptTheme();
|
|
@ -9,7 +9,7 @@ import { LibraryState } from '@/context/LibraryContext';
|
|||
import { ThemeState } from '@/context/ThemeContext';
|
||||
import { UsersState } from '@/context/UsersContext';
|
||||
|
||||
import ErrorFallback from './components/ErrorFallback';
|
||||
import ErrorFallback from './ErrorFallback';
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.js', import.meta.url).toString();
|
||||
|
60
rsconcept/frontend/src/app/Router.tsx
Normal file
60
rsconcept/frontend/src/app/Router.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { createBrowserRouter } from 'react-router-dom';
|
||||
|
||||
import CreateRSFormPage from '@/pages/CreateRSFormPage';
|
||||
import HomePage from '@/pages/HomePage';
|
||||
import LibraryPage from '@/pages/LibraryPage';
|
||||
import LoginPage from '@/pages/LoginPage';
|
||||
import ManualsPage from '@/pages/ManualsPage';
|
||||
import NotFoundPage from '@/pages/NotFoundPage';
|
||||
import RegisterPage from '@/pages/RegisterPage';
|
||||
import RestorePasswordPage from '@/pages/RestorePasswordPage';
|
||||
import RSFormPage from '@/pages/RSFormPage';
|
||||
import UserProfilePage from '@/pages/UserProfilePage';
|
||||
|
||||
import ApplicationLayout from './ApplicationLayout';
|
||||
|
||||
export const Router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <ApplicationLayout />,
|
||||
errorElement: <NotFoundPage />,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <HomePage />
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
element: <LoginPage />
|
||||
},
|
||||
{
|
||||
path: 'signup',
|
||||
element: <RegisterPage />
|
||||
},
|
||||
{
|
||||
path: 'restore-password',
|
||||
element: <RestorePasswordPage />
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
element: <UserProfilePage />
|
||||
},
|
||||
{
|
||||
path: 'manuals',
|
||||
element: <ManualsPage />
|
||||
},
|
||||
{
|
||||
path: 'library',
|
||||
element: <LibraryPage />
|
||||
},
|
||||
{
|
||||
path: 'library/create',
|
||||
element: <CreateRSFormPage />
|
||||
},
|
||||
{
|
||||
path: 'rsforms/:id',
|
||||
element: <RSFormPage />
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
9
rsconcept/frontend/src/app/index.tsx
Normal file
9
rsconcept/frontend/src/app/index.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import { Router } from './Router';
|
||||
|
||||
function App() {
|
||||
return <RouterProvider router={Router} />;
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -6,12 +6,21 @@ import { globalIDs } from '@/utils/constants';
|
|||
import { CheckboxCheckedIcon, CheckboxNullIcon } from '../Icons';
|
||||
import { CheckboxProps } from './Checkbox';
|
||||
|
||||
export interface TristateProps extends Omit<CheckboxProps, 'value' | 'setValue'> {
|
||||
export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'setValue'> {
|
||||
value: boolean | null;
|
||||
setValue?: (newValue: boolean | null) => void;
|
||||
}
|
||||
|
||||
function Tristate({ id, disabled, label, title, className, value, setValue, ...restProps }: TristateProps) {
|
||||
function CheckboxTristate({
|
||||
id,
|
||||
disabled,
|
||||
label,
|
||||
title,
|
||||
className,
|
||||
value,
|
||||
setValue,
|
||||
...restProps
|
||||
}: CheckboxTristateProps) {
|
||||
const cursor = useMemo(() => {
|
||||
if (disabled) {
|
||||
return 'cursor-not-allowed';
|
||||
|
@ -71,4 +80,4 @@ function Tristate({ id, disabled, label, title, className, value, setValue, ...r
|
|||
);
|
||||
}
|
||||
|
||||
export default Tristate;
|
||||
export default CheckboxTristate;
|
|
@ -4,11 +4,11 @@ import { ThreeDots } from 'react-loader-spinner';
|
|||
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
|
||||
interface ConceptLoaderProps {
|
||||
interface LoaderProps {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export function ConceptLoader({ size = 10 }: ConceptLoaderProps) {
|
||||
export function Loader({ size = 10 }: LoaderProps) {
|
||||
const { colors } = useConceptTheme();
|
||||
return (
|
||||
<div className='flex justify-center'>
|
|
@ -4,13 +4,13 @@ import { CProps } from '../props';
|
|||
import Overlay from './Overlay';
|
||||
import TextInput from './TextInput';
|
||||
|
||||
interface ConceptSearchProps extends CProps.Styling {
|
||||
interface SearchBarProps extends CProps.Styling {
|
||||
value: string;
|
||||
onChange?: (newValue: string) => void;
|
||||
noBorder?: boolean;
|
||||
}
|
||||
|
||||
function ConceptSearch({ value, onChange, noBorder, ...restProps }: ConceptSearchProps) {
|
||||
function SearchBar({ value, onChange, noBorder, ...restProps }: SearchBarProps) {
|
||||
return (
|
||||
<div {...restProps}>
|
||||
<Overlay position='top-[-0.125rem] left-3 translate-y-1/2' className='pointer-events-none clr-text-controls'>
|
||||
|
@ -19,6 +19,7 @@ function ConceptSearch({ value, onChange, noBorder, ...restProps }: ConceptSearc
|
|||
<TextInput
|
||||
noOutline
|
||||
placeholder='Поиск'
|
||||
type='search'
|
||||
className='w-full pl-10'
|
||||
noBorder={noBorder}
|
||||
value={value}
|
||||
|
@ -28,4 +29,4 @@ function ConceptSearch({ value, onChange, noBorder, ...restProps }: ConceptSearc
|
|||
);
|
||||
}
|
||||
|
||||
export default ConceptSearch;
|
||||
export default SearchBar;
|
|
@ -1,47 +0,0 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { CProps } from '../props';
|
||||
|
||||
interface SwitchButtonProps<ValueType> extends CProps.Styling {
|
||||
id?: string;
|
||||
value: ValueType;
|
||||
label?: string;
|
||||
icon?: React.ReactNode;
|
||||
title?: string;
|
||||
|
||||
isSelected?: boolean;
|
||||
onSelect: (value: ValueType) => void;
|
||||
}
|
||||
|
||||
function SwitchButton<ValueType>({
|
||||
value,
|
||||
icon,
|
||||
label,
|
||||
className,
|
||||
isSelected,
|
||||
onSelect,
|
||||
...restProps
|
||||
}: SwitchButtonProps<ValueType>) {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
tabIndex={-1}
|
||||
onClick={() => onSelect(value)}
|
||||
className={clsx(
|
||||
'px-2 py-1',
|
||||
'border rounded-none',
|
||||
'font-controls',
|
||||
'clr-btn-clear clr-hover',
|
||||
'cursor-pointer',
|
||||
isSelected && 'clr-selected',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{icon ? icon : null}
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default SwitchButton;
|
|
@ -1,16 +1,16 @@
|
|||
import clsx from 'clsx';
|
||||
import type { TabProps } from 'react-tabs';
|
||||
import { Tab } from 'react-tabs';
|
||||
import type { TabProps as TabPropsImpl } from 'react-tabs';
|
||||
import { Tab as TabImpl } from 'react-tabs';
|
||||
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
interface ConceptTabProps extends Omit<TabProps, 'children'> {
|
||||
interface TabLabelProps extends Omit<TabPropsImpl, 'children'> {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
function ConceptTab({ label, title, className, ...otherProps }: ConceptTabProps) {
|
||||
function TabLabel({ label, title, className, ...otherProps }: TabLabelProps) {
|
||||
return (
|
||||
<Tab
|
||||
<TabImpl
|
||||
className={clsx(
|
||||
'min-w-[6rem]',
|
||||
'px-2 py-1 flex justify-center',
|
||||
|
@ -24,10 +24,10 @@ function ConceptTab({ label, title, className, ...otherProps }: ConceptTabProps)
|
|||
{...otherProps}
|
||||
>
|
||||
{label}
|
||||
</Tab>
|
||||
</TabImpl>
|
||||
);
|
||||
}
|
||||
|
||||
ConceptTab.tabsRole = 'Tab';
|
||||
TabLabel.tabsRole = 'Tab';
|
||||
|
||||
export default ConceptTab;
|
||||
export default TabLabel;
|
|
@ -3,16 +3,16 @@
|
|||
import clsx from 'clsx';
|
||||
import { ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ITooltip, Tooltip } from 'react-tooltip';
|
||||
import { ITooltip, Tooltip as TooltipImpl } from 'react-tooltip';
|
||||
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
|
||||
interface ConceptTooltipProps extends Omit<ITooltip, 'variant'> {
|
||||
interface TooltipProps extends Omit<ITooltip, 'variant'> {
|
||||
layer?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
function ConceptTooltip({
|
||||
function Tooltip({
|
||||
text,
|
||||
children,
|
||||
layer = 'z-tooltip',
|
||||
|
@ -20,13 +20,13 @@ function ConceptTooltip({
|
|||
className,
|
||||
style,
|
||||
...restProps
|
||||
}: ConceptTooltipProps) {
|
||||
}: TooltipProps) {
|
||||
const { darkMode } = useConceptTheme();
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
return createPortal(
|
||||
<Tooltip
|
||||
<TooltipImpl
|
||||
delayShow={1000}
|
||||
delayHide={100}
|
||||
opacity={0.97}
|
||||
|
@ -39,9 +39,9 @@ function ConceptTooltip({
|
|||
>
|
||||
{text ? text : null}
|
||||
{children as ReactNode}
|
||||
</Tooltip>,
|
||||
</TooltipImpl>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
export default ConceptTooltip;
|
||||
export default Tooltip;
|
|
@ -1,6 +1,6 @@
|
|||
import { Table } from '@tanstack/react-table';
|
||||
|
||||
import Tristate from '@/components/Common/Tristate';
|
||||
import CheckboxTristate from '@/components/Common/CheckboxTristate';
|
||||
|
||||
interface SelectAllProps<TData> {
|
||||
table: Table<TData>;
|
||||
|
@ -8,7 +8,7 @@ interface SelectAllProps<TData> {
|
|||
|
||||
function SelectAll<TData>({ table }: SelectAllProps<TData>) {
|
||||
return (
|
||||
<Tristate
|
||||
<CheckboxTristate
|
||||
tabIndex={-1}
|
||||
title='Выделить все'
|
||||
value={
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import ConceptTooltip from '@/components/Common/ConceptTooltip';
|
||||
import Tooltip from '@/components/Common/Tooltip';
|
||||
import InfoConstituenta from '@/components/Shared/InfoConstituenta';
|
||||
import { IConstituenta } from '@/models/rsform';
|
||||
|
||||
|
@ -9,9 +9,9 @@ interface ConstituentaTooltipProps {
|
|||
|
||||
function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
|
||||
return (
|
||||
<ConceptTooltip clickable anchorSelect={anchor} className='max-w-[30rem]'>
|
||||
<Tooltip clickable anchorSelect={anchor} className='max-w-[30rem]'>
|
||||
<InfoConstituenta data={data} />
|
||||
</ConceptTooltip>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,9 +7,14 @@ function HelpAPI() {
|
|||
<h1>Программный интерфейс Портала</h1>
|
||||
<p>В качестве программного интерфейса сервера используется REST API, реализованный с помощью Django.</p>
|
||||
<p>На данный момент API находится в разработке, поэтому поддержка внешних запросов не производится.</p>
|
||||
<p>С описанием интерфейса можно ознакомиться <TextURL text='по ссылке' href={urls.restAPI}/>.</p>
|
||||
<p><TextURL text='Принять участие в разработке' href={urls.git_repo}/></p>
|
||||
</div>);
|
||||
<p>
|
||||
С описанием интерфейса можно ознакомиться <TextURL text='по ссылке' href={urls.restAPI} />.
|
||||
</p>
|
||||
<p>
|
||||
<TextURL text='Принять участие в разработке' href={urls.git_repo} />
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HelpAPI;
|
|
@ -1,7 +1,7 @@
|
|||
import { BiInfoCircle } from 'react-icons/bi';
|
||||
|
||||
import ConceptTooltip from '@/components/Common/ConceptTooltip';
|
||||
import TextURL from '@/components/Common/TextURL';
|
||||
import Tooltip from '@/components/Common/Tooltip';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
||||
import { CProps } from '../props';
|
||||
|
@ -16,14 +16,14 @@ function HelpButton({ topic, ...restProps }: HelpButtonProps) {
|
|||
return (
|
||||
<div id={`help-${topic}`} className='p-1'>
|
||||
<BiInfoCircle size='1.25rem' className='clr-text-primary' />
|
||||
<ConceptTooltip clickable anchorSelect={`#help-${topic}`} layer='z-modal-tooltip' {...restProps}>
|
||||
<Tooltip clickable anchorSelect={`#help-${topic}`} layer='z-modal-tooltip' {...restProps}>
|
||||
<div className='relative'>
|
||||
<div className='absolute right-0 text-sm top-[0.4rem]'>
|
||||
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
|
||||
</div>
|
||||
</div>
|
||||
<InfoTopic topic={topic} />
|
||||
</ConceptTooltip>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import ConceptSearch from '@/components/Common/ConceptSearch';
|
||||
import SearchBar from '@/components/Common/SearchBar';
|
||||
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/DataTable';
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import { CstMatchMode } from '@/models/miscellaneous';
|
||||
|
@ -83,7 +83,7 @@ function ConstituentaPicker({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<ConceptSearch value={filterText} onChange={newValue => setFilterText(newValue)} />
|
||||
<SearchBar value={filterText} onChange={newValue => setFilterText(newValue)} />
|
||||
<DataTable
|
||||
dense
|
||||
noHeader
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import clsx from 'clsx';
|
||||
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
|
||||
|
||||
import ConceptTooltip from '@/components/Common/ConceptTooltip';
|
||||
import Tooltip from '@/components/Common/Tooltip';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { animationDuration } from '@/utils/animations';
|
||||
import { darkT, IColorTheme, lightT } from '@/utils/color';
|
||||
|
@ -104,7 +104,7 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
|
|||
}}
|
||||
>
|
||||
<>
|
||||
<ConceptTooltip
|
||||
<Tooltip
|
||||
float
|
||||
id={`${globalIDs.tooltip}`}
|
||||
layer='z-topmost'
|
||||
|
|
|
@ -4,9 +4,9 @@ import clsx from 'clsx';
|
|||
import { useLayoutEffect, useState } from 'react';
|
||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
|
||||
import ConceptTab from '@/components/Common/ConceptTab';
|
||||
import Modal, { ModalProps } from '@/components/Common/Modal';
|
||||
import Overlay from '@/components/Common/Overlay';
|
||||
import TabLabel from '@/components/Common/TabLabel';
|
||||
import HelpButton from '@/components/Help/HelpButton';
|
||||
import usePartialUpdate from '@/hooks/usePartialUpdate';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
@ -125,9 +125,9 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
|
|||
onSelect={setActiveTab}
|
||||
>
|
||||
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}>
|
||||
<ConceptTab label='Шаблон' title='Выбор шаблона выражения' className='w-[8rem]' />
|
||||
<ConceptTab label='Аргументы' title='Подстановка аргументов шаблона' className='w-[8rem]' />
|
||||
<ConceptTab label='Конституента' title='Редактирование атрибутов конституенты' className='w-[8rem]' />
|
||||
<TabLabel label='Шаблон' title='Выбор шаблона выражения' className='w-[8rem]' />
|
||||
<TabLabel label='Аргументы' title='Подстановка аргументов шаблона' className='w-[8rem]' />
|
||||
<TabLabel label='Конституента' title='Редактирование атрибутов конституенты' className='w-[8rem]' />
|
||||
</TabList>
|
||||
|
||||
<TabPanel style={{ display: activeTab === TabID.TEMPLATE ? '' : 'none' }}>
|
||||
|
|
|
@ -4,7 +4,7 @@ import clsx from 'clsx';
|
|||
import { useState } from 'react';
|
||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
|
||||
import ConceptTab from '@/components/Common/ConceptTab';
|
||||
import TabLabel from '@/components/Common/TabLabel';
|
||||
import Modal from '@/components/Common/Modal';
|
||||
import Overlay from '@/components/Common/Overlay';
|
||||
import HelpButton from '@/components/Help/HelpButton';
|
||||
|
@ -64,12 +64,12 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
|
|||
onSelect={setActiveTab}
|
||||
>
|
||||
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}>
|
||||
<ConceptTab
|
||||
<TabLabel
|
||||
title='Отсылка на термин в заданной словоформе'
|
||||
label={labelReferenceType(ReferenceType.ENTITY)}
|
||||
className='w-[12rem]'
|
||||
/>
|
||||
<ConceptTab
|
||||
<TabLabel
|
||||
title='Установление синтаксической связи с отсылкой на термин'
|
||||
label={labelReferenceType(ReferenceType.SYNTACTIC)}
|
||||
className='w-[12rem]'
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import GraphUI, { GraphEdge, GraphNode } from '@/components/Common/GraphUI';
|
||||
import Modal, { ModalProps } from '@/components/Common/Modal';
|
||||
import GraphUI, { GraphEdge, GraphNode } from '@/components/GraphUI';
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import { SyntaxTree } from '@/models/rslang';
|
||||
import { graphDarkT, graphLightT } from '@/utils/color';
|
||||
|
|
|
@ -2,8 +2,8 @@ import './index.css';
|
|||
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import App from './App.tsx';
|
||||
import GlobalProviders from './GlobalProviders';
|
||||
import App from './app';
|
||||
import GlobalProviders from './app/GlobalProviders';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<GlobalProviders>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { ConceptLoader } from '@/components/Common/ConceptLoader';
|
||||
import { Loader } from '@/components/Common/Loader';
|
||||
import InfoError from '@/components/InfoError';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
|
@ -65,7 +65,7 @@ function LibraryPage() {
|
|||
|
||||
return (
|
||||
<>
|
||||
{library.loading ? <ConceptLoader /> : null}
|
||||
{library.loading ? <Loader /> : null}
|
||||
{library.error ? <InfoError error={library.error} /> : null}
|
||||
{!library.loading && library.items ? (
|
||||
<>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import clsx from 'clsx';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import ConceptSearch from '@/components/Common/ConceptSearch';
|
||||
import SearchBar from '@/components/Common/SearchBar';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||
import { LibraryFilterStrategy } from '@/models/miscellaneous';
|
||||
|
@ -67,7 +67,7 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setFilter }:
|
|||
</span>
|
||||
</div>
|
||||
<div className={clsx('flex-grow', 'flex gap-1 justify-center items-center')}>
|
||||
<ConceptSearch noBorder className='min-w-[10rem]' value={query} onChange={handleChangeQuery} />
|
||||
<SearchBar noBorder className='min-w-[10rem]' value={query} onChange={handleChangeQuery} />
|
||||
<PickerStrategy value={strategy} onChange={handleChangeStrategy} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { ConceptLoader } from '@/components/Common/ConceptLoader';
|
||||
import { Loader } from '@/components/Common/Loader';
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import { ExpressionStatus } from '@/models/rsform';
|
||||
import { type IConstituenta } from '@/models/rsform';
|
||||
|
@ -52,7 +52,7 @@ function StatusBar({ isModified, processing, constituenta, parseData, onAnalyze
|
|||
onClick={onAnalyze}
|
||||
>
|
||||
{processing ? (
|
||||
<ConceptLoader size={3} />
|
||||
<Loader size={3} />
|
||||
) : (
|
||||
<>
|
||||
<StatusIcon status={status} />
|
||||
|
|
|
@ -14,21 +14,26 @@ import RSFormStats from './RSFormStats';
|
|||
import RSFormToolbar from './RSFormToolbar';
|
||||
|
||||
interface EditorRSFormProps {
|
||||
isModified: boolean
|
||||
isMutable: boolean
|
||||
isModified: boolean;
|
||||
isMutable: boolean;
|
||||
|
||||
setIsModified: Dispatch<SetStateAction<boolean>>
|
||||
onDestroy: () => void
|
||||
onClaim: () => void
|
||||
onShare: () => void
|
||||
onDownload: () => void
|
||||
onToggleSubscribe: () => void
|
||||
setIsModified: Dispatch<SetStateAction<boolean>>;
|
||||
onDestroy: () => void;
|
||||
onClaim: () => void;
|
||||
onShare: () => void;
|
||||
onDownload: () => void;
|
||||
onToggleSubscribe: () => void;
|
||||
}
|
||||
|
||||
function EditorRSForm({
|
||||
isModified, isMutable,
|
||||
onDestroy, onClaim, onShare, setIsModified,
|
||||
onDownload, onToggleSubscribe
|
||||
isModified,
|
||||
isMutable,
|
||||
onDestroy,
|
||||
onClaim,
|
||||
onShare,
|
||||
setIsModified,
|
||||
onDownload,
|
||||
onToggleSubscribe
|
||||
}: EditorRSFormProps) {
|
||||
const { schema, isClaimable, isSubscribed, processing } = useRSForm();
|
||||
const { user } = useAuth();
|
||||
|
@ -58,7 +63,6 @@ function EditorRSForm({
|
|||
modified={isModified}
|
||||
claimable={isClaimable}
|
||||
anonymous={!user}
|
||||
|
||||
onSubmit={initiateSubmit}
|
||||
onShare={onShare}
|
||||
onDownload={onDownload}
|
||||
|
@ -66,12 +70,10 @@ function EditorRSForm({
|
|||
onDestroy={onDestroy}
|
||||
onToggleSubscribe={onToggleSubscribe}
|
||||
/>
|
||||
<div tabIndex={-1}
|
||||
className='flex'
|
||||
onKeyDown={handleInput}
|
||||
>
|
||||
<div tabIndex={-1} className='flex' onKeyDown={handleInput}>
|
||||
<FlexColumn className='px-4 pb-2'>
|
||||
<FormRSForm disabled={!isMutable}
|
||||
<FormRSForm
|
||||
disabled={!isMutable}
|
||||
id={globalIDs.library_item_editor}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
|
@ -84,7 +86,8 @@ function EditorRSForm({
|
|||
|
||||
<RSFormStats stats={schema?.stats} />
|
||||
</div>
|
||||
</>);
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditorRSForm;
|
|
@ -3,7 +3,7 @@ import LabeledValue from '@/components/Common/LabeledValue';
|
|||
import { type IRSFormStats } from '@/models/rsform';
|
||||
|
||||
interface RSFormStatsProps {
|
||||
stats?: IRSFormStats
|
||||
stats?: IRSFormStats;
|
||||
}
|
||||
|
||||
function RSFormStats({ stats }: RSFormStatsProps) {
|
||||
|
@ -12,83 +12,45 @@ function RSFormStats({ stats }: RSFormStatsProps) {
|
|||
}
|
||||
return (
|
||||
<div className='flex flex-col gap-1 px-4 mt-8 w-[16rem]'>
|
||||
<LabeledValue id='count_all'
|
||||
label='Всего конституент '
|
||||
text={stats.count_all}
|
||||
/>
|
||||
<LabeledValue id='count_errors'
|
||||
label='Некорректных'
|
||||
text={stats.count_errors}
|
||||
/>
|
||||
{stats.count_property !== 0 ?
|
||||
<LabeledValue id='count_property'
|
||||
label='Неразмерных'
|
||||
text={stats.count_property}
|
||||
/> : null}
|
||||
{stats.count_incalculable !== 0 ?
|
||||
<LabeledValue id='count_incalculable'
|
||||
label='Невычислимых'
|
||||
text={stats.count_incalculable}
|
||||
/> : null}
|
||||
<LabeledValue id='count_all' label='Всего конституент ' text={stats.count_all} />
|
||||
<LabeledValue id='count_errors' label='Некорректных' text={stats.count_errors} />
|
||||
{stats.count_property !== 0 ? (
|
||||
<LabeledValue id='count_property' label='Неразмерных' text={stats.count_property} />
|
||||
) : null}
|
||||
{stats.count_incalculable !== 0 ? (
|
||||
<LabeledValue id='count_incalculable' label='Невычислимых' text={stats.count_incalculable} />
|
||||
) : null}
|
||||
|
||||
<Divider margins='my-2' />
|
||||
|
||||
<LabeledValue id='count_text_term'
|
||||
label='Термины'
|
||||
text={stats.count_text_term}
|
||||
/>
|
||||
<LabeledValue id='count_definition'
|
||||
label='Определения'
|
||||
text={stats.count_definition}
|
||||
/>
|
||||
<LabeledValue id='count_convention'
|
||||
label='Конвенции'
|
||||
text={stats.count_convention}
|
||||
/>
|
||||
<LabeledValue id='count_text_term' label='Термины' text={stats.count_text_term} />
|
||||
<LabeledValue id='count_definition' label='Определения' text={stats.count_definition} />
|
||||
<LabeledValue id='count_convention' label='Конвенции' text={stats.count_convention} />
|
||||
|
||||
<Divider margins='my-2' />
|
||||
|
||||
{stats.count_base !== 0 ?
|
||||
<LabeledValue id='count_base'
|
||||
label='Базисные множества '
|
||||
text={stats.count_base}
|
||||
/> : null}
|
||||
{ stats.count_constant !== 0 ?
|
||||
<LabeledValue id='count_constant'
|
||||
label='Константные множества '
|
||||
text={stats.count_constant}
|
||||
/> : null}
|
||||
{stats.count_structured !== 0 ?
|
||||
<LabeledValue id='count_structured'
|
||||
label='Родовые структуры '
|
||||
text={stats.count_structured}
|
||||
/> : null}
|
||||
{stats.count_axiom !== 0 ?
|
||||
<LabeledValue id='count_axiom'
|
||||
label='Аксиомы '
|
||||
text={stats.count_axiom}
|
||||
/> : null}
|
||||
{stats.count_term !== 0 ?
|
||||
<LabeledValue id='count_term'
|
||||
label='Термы '
|
||||
text={stats.count_term}
|
||||
/> : null}
|
||||
{stats.count_function !== 0 ?
|
||||
<LabeledValue id='count_function'
|
||||
label='Терм-функции '
|
||||
text={stats.count_function}
|
||||
/> : null}
|
||||
{stats.count_predicate !== 0 ?
|
||||
<LabeledValue id='count_predicate'
|
||||
label='Предикат-функции '
|
||||
text={stats.count_predicate}
|
||||
/> : null}
|
||||
{stats.count_theorem !== 0 ?
|
||||
<LabeledValue id='count_theorem'
|
||||
label='Теоремы '
|
||||
text={stats.count_theorem}
|
||||
/> : null}
|
||||
</div>);
|
||||
{stats.count_base !== 0 ? (
|
||||
<LabeledValue id='count_base' label='Базисные множества ' text={stats.count_base} />
|
||||
) : null}
|
||||
{stats.count_constant !== 0 ? (
|
||||
<LabeledValue id='count_constant' label='Константные множества ' text={stats.count_constant} />
|
||||
) : null}
|
||||
{stats.count_structured !== 0 ? (
|
||||
<LabeledValue id='count_structured' label='Родовые структуры ' text={stats.count_structured} />
|
||||
) : null}
|
||||
{stats.count_axiom !== 0 ? <LabeledValue id='count_axiom' label='Аксиомы ' text={stats.count_axiom} /> : null}
|
||||
{stats.count_term !== 0 ? <LabeledValue id='count_term' label='Термы ' text={stats.count_term} /> : null}
|
||||
{stats.count_function !== 0 ? (
|
||||
<LabeledValue id='count_function' label='Терм-функции ' text={stats.count_function} />
|
||||
) : null}
|
||||
{stats.count_predicate !== 0 ? (
|
||||
<LabeledValue id='count_predicate' label='Предикат-функции ' text={stats.count_predicate} />
|
||||
) : null}
|
||||
{stats.count_theorem !== 0 ? (
|
||||
<LabeledValue id='count_theorem' label='Теоремы ' text={stats.count_theorem} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RSFormStats;
|
|
@ -11,28 +11,36 @@ import HelpButton from '@/components/Help/HelpButton';
|
|||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
||||
interface RSFormToolbarProps {
|
||||
isMutable: boolean
|
||||
isSubscribed: boolean
|
||||
modified: boolean
|
||||
claimable: boolean
|
||||
anonymous: boolean
|
||||
processing: boolean
|
||||
isMutable: boolean;
|
||||
isSubscribed: boolean;
|
||||
modified: boolean;
|
||||
claimable: boolean;
|
||||
anonymous: boolean;
|
||||
processing: boolean;
|
||||
|
||||
onSubmit: () => void
|
||||
onShare: () => void
|
||||
onDownload: () => void
|
||||
onClaim: () => void
|
||||
onDestroy: () => void
|
||||
onToggleSubscribe: () => void
|
||||
onSubmit: () => void;
|
||||
onShare: () => void;
|
||||
onDownload: () => void;
|
||||
onClaim: () => void;
|
||||
onDestroy: () => void;
|
||||
onToggleSubscribe: () => void;
|
||||
}
|
||||
|
||||
function RSFormToolbar({
|
||||
isMutable, modified, claimable, anonymous,
|
||||
isSubscribed, onToggleSubscribe, processing,
|
||||
onSubmit, onShare, onDownload,
|
||||
onClaim, onDestroy
|
||||
isMutable,
|
||||
modified,
|
||||
claimable,
|
||||
anonymous,
|
||||
isSubscribed,
|
||||
onToggleSubscribe,
|
||||
processing,
|
||||
onSubmit,
|
||||
onShare,
|
||||
onDownload,
|
||||
onClaim,
|
||||
onDestroy
|
||||
}: RSFormToolbarProps) {
|
||||
const canSave = useMemo(() => (modified && isMutable), [modified, isMutable]);
|
||||
const canSave = useMemo(() => modified && isMutable, [modified, isMutable]);
|
||||
return (
|
||||
<Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'>
|
||||
<MiniButton
|
||||
|
@ -54,9 +62,12 @@ function RSFormToolbar({
|
|||
<MiniButton
|
||||
title={`Отслеживание ${isSubscribed ? 'включено' : 'выключено'}`}
|
||||
disabled={anonymous || processing}
|
||||
icon={isSubscribed
|
||||
? <FiBell size='1.25rem' className='clr-text-primary' />
|
||||
: <FiBellOff size='1.25rem' className='clr-text-controls' />
|
||||
icon={
|
||||
isSubscribed ? (
|
||||
<FiBell size='1.25rem' className='clr-text-primary' />
|
||||
) : (
|
||||
<FiBellOff size='1.25rem' className='clr-text-controls' />
|
||||
)
|
||||
}
|
||||
style={{ outlineColor: 'transparent' }}
|
||||
onClick={onToggleSubscribe}
|
||||
|
@ -74,7 +85,8 @@ function RSFormToolbar({
|
|||
icon={<BiTrash size='1.25rem' className={isMutable ? 'clr-text-warning' : ''} />}
|
||||
/>
|
||||
<HelpButton topic={HelpTopic.RSFORM} offset={4} />
|
||||
</Overlay>);
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
||||
export default RSFormToolbar;
|
|
@ -2,14 +2,7 @@
|
|||
|
||||
import { useCallback, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import GraphUI, {
|
||||
GraphCanvasRef,
|
||||
GraphEdge,
|
||||
GraphNode,
|
||||
LayoutTypes,
|
||||
Sphere,
|
||||
useSelection
|
||||
} from '@/components/Common/GraphUI';
|
||||
import GraphUI, { GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, Sphere, useSelection } from '@/components/GraphUI';
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import { graphDarkT, graphLightT } from '@/utils/color';
|
||||
import { resources } from '@/utils/constants';
|
||||
|
|
|
@ -8,8 +8,8 @@ import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
|||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { ConceptLoader } from '@/components/Common/ConceptLoader';
|
||||
import ConceptTab from '@/components/Common/ConceptTab';
|
||||
import { Loader } from '@/components/Common/Loader';
|
||||
import TabLabel from '@/components/Common/TabLabel';
|
||||
import TextURL from '@/components/Common/TextURL';
|
||||
import InfoError, { ErrorData } from '@/components/InfoError';
|
||||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
|
@ -360,7 +360,7 @@ function RSTabs() {
|
|||
|
||||
return (
|
||||
<>
|
||||
{loading ? <ConceptLoader /> : null}
|
||||
{loading ? <Loader /> : null}
|
||||
{error ? <ProcessError error={error} /> : null}
|
||||
<AnimatePresence>
|
||||
{showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null}
|
||||
|
@ -425,13 +425,13 @@ function RSTabs() {
|
|||
showCloneDialog={promptClone}
|
||||
showUploadDialog={() => setShowUpload(true)}
|
||||
/>
|
||||
<ConceptTab label='Карточка' title={`Название схемы: ${schema.title ?? ''}`} />
|
||||
<ConceptTab
|
||||
<TabLabel label='Карточка' title={`Название схемы: ${schema.title ?? ''}`} />
|
||||
<TabLabel
|
||||
label='Содержание'
|
||||
title={`Конституент: ${schema.stats?.count_all ?? 0} | Ошибок: ${schema.stats?.count_errors ?? 0}`}
|
||||
/>
|
||||
<ConceptTab label='Редактор' />
|
||||
<ConceptTab label='Граф термов' />
|
||||
<TabLabel label='Редактор' />
|
||||
<TabLabel label='Граф термов' />
|
||||
</TabList>
|
||||
|
||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '' : 'none' }}>
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
import { useCallback, useLayoutEffect } from 'react';
|
||||
import { BiCog, BiFilterAlt } from 'react-icons/bi';
|
||||
|
||||
import ConceptSearch from '@/components/Common/ConceptSearch';
|
||||
import Dropdown from '@/components/Common/Dropdown';
|
||||
import DropdownButton from '@/components/Common/DropdownButton';
|
||||
import SearchBar from '@/components/Common/SearchBar';
|
||||
import SelectorButton from '@/components/Common/SelectorButton';
|
||||
import useDropdown from '@/hooks/useDropdown';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
|
@ -75,7 +75,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
|
|||
|
||||
return (
|
||||
<div className='flex border-b clr-input'>
|
||||
<ConceptSearch noBorder className='min-w-[6rem] pr-2 flex-grow' value={filterText} onChange={setFilterText} />
|
||||
<SearchBar noBorder className='min-w-[6rem] pr-2 flex-grow' value={filterText} onChange={setFilterText} />
|
||||
|
||||
<div ref={matchModeMenu.ref}>
|
||||
<SelectorButton
|
||||
|
|
|
@ -7,12 +7,12 @@ import { toast } from 'react-toastify';
|
|||
|
||||
import Button from '@/components/Common/Button';
|
||||
import Checkbox from '@/components/Common/Checkbox';
|
||||
import ConceptTooltip from '@/components/Common/ConceptTooltip';
|
||||
import FlexColumn from '@/components/Common/FlexColumn';
|
||||
import Overlay from '@/components/Common/Overlay';
|
||||
import SubmitButton from '@/components/Common/SubmitButton';
|
||||
import TextInput from '@/components/Common/TextInput';
|
||||
import TextURL from '@/components/Common/TextURL';
|
||||
import Tooltip from '@/components/Common/Tooltip';
|
||||
import ExpectedAnonymous from '@/components/ExpectedAnonymous';
|
||||
import InfoError from '@/components/InfoError';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
|
@ -75,10 +75,10 @@ function RegisterPage() {
|
|||
<Overlay id={globalIDs.password_tooltip} position='top-[4.8rem] left-[3.4rem] absolute'>
|
||||
<BiInfoCircle size='1.25rem' className='clr-text-primary' />
|
||||
</Overlay>
|
||||
<ConceptTooltip anchorSelect={`#${globalIDs.password_tooltip}`} offset={6}>
|
||||
<Tooltip anchorSelect={`#${globalIDs.password_tooltip}`} offset={6}>
|
||||
<p>- используйте уникальный пароль</p>
|
||||
<p>- портал функционирует в тестовом режиме</p>
|
||||
</ConceptTooltip>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
|
|
|
@ -4,7 +4,7 @@ import { AnimatePresence } from 'framer-motion';
|
|||
import { useMemo, useState } from 'react';
|
||||
import { FiBell, FiBellOff } from 'react-icons/fi';
|
||||
|
||||
import { ConceptLoader } from '@/components/Common/ConceptLoader';
|
||||
import { Loader } from '@/components/Common/Loader';
|
||||
import MiniButton from '@/components/Common/MiniButton';
|
||||
import Overlay from '@/components/Common/Overlay';
|
||||
import InfoError from '@/components/InfoError';
|
||||
|
@ -29,7 +29,7 @@ function UserTabs() {
|
|||
|
||||
return (
|
||||
<>
|
||||
{loading ? <ConceptLoader /> : null}
|
||||
{loading ? <Loader /> : null}
|
||||
{error ? <InfoError error={error} /> : null}
|
||||
{user ? (
|
||||
<div className='flex gap-6 py-2'>
|
||||
|
|
Loading…
Reference in New Issue
Block a user