Refactoring: apply prettier on save

This commit is contained in:
IRBorisov 2023-12-28 14:04:44 +03:00
parent 87d3152e6c
commit 40cb8b4ce8
222 changed files with 7652 additions and 7666 deletions

10
.vscode/settings.json vendored
View File

@ -3,13 +3,7 @@
".mypy_cache/": true,
".pytest_cache/": true
},
"python.testing.unittestArgs": [
"-v",
"-s",
"./tests",
"-p",
"test*.py"
],
"python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test*.py"],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true,
"eslint.workingDirectories": [
@ -57,6 +51,7 @@
"datv",
"Debool",
"Decart",
"Downvote",
"EMPTYSET",
"exteor",
"femn",
@ -106,6 +101,7 @@
"tanstack",
"toastify",
"tooltipic",
"Upvote",
"Viewset",
"viewsets",
"wordform",

View File

@ -0,0 +1,13 @@
{
"semi": true,
"useTabs": false,
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "none",
"arrowParens": "avoid",
"singleQuote": true,
"jsxSingleQuote": true,
"quoteProps": "consistent",
"bracketSameLine": false,
"bracketSpacing": true
}

View File

@ -22,9 +22,8 @@ function Root() {
return (
<NavigationState>
<div className='min-w-[30rem] clr-app antialiased'>
<ConceptToaster
className='mt-[4rem] text-sm'
className='mt-[4rem] text-sm' //
autoClose={3000}
draggable={false}
pauseOnFocusLoss={false}
@ -32,23 +31,22 @@ function Root() {
<Navigation />
<div id={globalIDs.main_scroll}
className='overscroll-none min-w-fit overflow-y-auto'
<div
id={globalIDs.main_scroll}
className='overflow-y-auto overscroll-none min-w-fit'
style={{
maxHeight: viewportHeight,
overflowY: showScroll ? 'scroll': 'auto'
overflowY: showScroll ? 'scroll' : 'auto'
}}
>
<main
className='flex flex-col items-center'
style={{minHeight: mainHeight}}
>
<main className='flex flex-col items-center' style={{ minHeight: mainHeight }}>
<Outlet />
</main>
<Footer />
</div>
</div>
</NavigationState>);
</NavigationState>
);
}
const router = createBrowserRouter([
@ -59,48 +57,46 @@ const router = createBrowserRouter([
children: [
{
path: '',
element: <HomePage />,
element: <HomePage />
},
{
path: 'login',
element: <LoginPage />,
element: <LoginPage />
},
{
path: 'signup',
element: <RegisterPage />,
element: <RegisterPage />
},
{
path: 'restore-password',
element: <RestorePasswordPage />,
element: <RestorePasswordPage />
},
{
path: 'profile',
element: <UserProfilePage />,
element: <UserProfilePage />
},
{
path: 'manuals',
element: <ManualsPage />,
element: <ManualsPage />
},
{
path: 'library',
element: <LibraryPage />,
element: <LibraryPage />
},
{
path: 'library/create',
element: <CreateRSFormPage />,
element: <CreateRSFormPage />
},
{
path: 'rsforms/:id',
element: <RSFormPage />,
},
element: <RSFormPage />
}
]
},
}
]);
function App () {
return (
<RouterProvider router={router} />
);
function App() {
return <RouterProvider router={router} />;
}
export default App;

View File

@ -11,13 +11,10 @@ import { UsersState } from '@/context/UsersContext';
import ErrorFallback from './components/ErrorFallback';
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.js',
import.meta.url,
).toString();
pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.js', import.meta.url).toString();
const resetState = () => {
console.log('Resetting state after error fallback')
console.log('Resetting state after error fallback');
};
const logError = (error: Error, info: { componentStack?: string | null | undefined }) => {
@ -27,6 +24,7 @@ const logError = (error: Error, info: { componentStack?: string | null | undefin
}
};
// prettier-ignore
function GlobalProviders({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundary

View File

@ -4,24 +4,30 @@ import { globalIDs } from '@/utils/constants';
import { CProps } from '../props';
interface ButtonProps
extends CProps.Control, CProps.Colors, CProps.Button {
text?: string
icon?: React.ReactNode
interface ButtonProps extends CProps.Control, CProps.Colors, CProps.Button {
text?: string;
icon?: React.ReactNode;
dense?: boolean
loading?: boolean
dense?: boolean;
loading?: boolean;
}
function Button({
text, icon, title,
loading, dense, disabled, noBorder, noOutline,
text,
icon,
title,
loading,
dense,
disabled,
noBorder,
noOutline,
colors = 'clr-btn-default',
className,
...restProps
}: ButtonProps) {
return (
<button type='button'
<button
type='button'
disabled={disabled ?? loading}
className={clsx(
'inline-flex gap-2 items-center justify-center',
@ -33,7 +39,7 @@ function Button({
'cursor-progress': loading,
'cursor-pointer': !loading,
'outline-none': noOutline,
'clr-outline': !noOutline,
'clr-outline': !noOutline
},
className,
colors
@ -44,7 +50,8 @@ function Button({
>
{icon ? icon : null}
{text ? <span className='font-semibold'>{text}</span> : null}
</button>);
</button>
);
}
export default Button;

View File

@ -7,21 +7,16 @@ import { CheckboxCheckedIcon } from '../Icons';
import { CProps } from '../props';
import Label from './Label';
export interface CheckboxProps
extends Omit<CProps.Button, 'value' | 'onClick'> {
label?: string
disabled?: boolean
export interface CheckboxProps extends Omit<CProps.Button, 'value' | 'onClick'> {
label?: string;
disabled?: boolean;
value: boolean
setValue?: (newValue: boolean) => void
value: boolean;
setValue?: (newValue: boolean) => void;
}
function Checkbox({
id, disabled, label, title,
className, value, setValue, ...restProps
}: CheckboxProps) {
const cursor = useMemo(
() => {
function Checkbox({ id, disabled, label, title, className, value, setValue, ...restProps }: CheckboxProps) {
const cursor = useMemo(() => {
if (disabled) {
return 'cursor-not-allowed';
} else if (setValue) {
@ -40,32 +35,31 @@ function Checkbox({
}
return (
<button type='button' id={id}
className={clsx(
'flex items-center gap-2',
'outline-none',
'text-start',
cursor,
className
)}
<button
type='button'
id={id}
className={clsx('flex items-center gap-2', 'outline-none', 'text-start', cursor, className)}
disabled={disabled}
onClick={handleClick}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
data-tooltip-id={title ? globalIDs.tooltip : undefined}
data-tooltip-content={title}
{...restProps}
>
<div className={clsx(
'max-w-[1rem] min-w-[1rem] h-4',
'border rounded-sm',
{
<div
className={clsx('max-w-[1rem] min-w-[1rem] h-4', 'border rounded-sm', {
'clr-primary': value !== false,
'clr-app': value === false
}
)}>
{value ? <div className='mt-[1px] ml-[1px]'><CheckboxCheckedIcon /></div> : null}
})}
>
{value ? (
<div className='mt-[1px] ml-[1px]'>
<CheckboxCheckedIcon />
</div>
) : null}
</div>
<Label className={cursor} text={label} htmlFor={id} />
</button>);
</button>
);
}
export default Checkbox;

View File

@ -5,18 +5,14 @@ import { ThreeDots } from 'react-loader-spinner';
import { useConceptTheme } from '@/context/ThemeContext';
interface ConceptLoaderProps {
size?: number
size?: number;
}
export function ConceptLoader({size=10}: ConceptLoaderProps) {
const {colors} = useConceptTheme();
export function ConceptLoader({ size = 10 }: ConceptLoaderProps) {
const { colors } = useConceptTheme();
return (
<div className='flex justify-center'>
<ThreeDots
color={colors.bgSelected}
height={size*10}
width={size*10}
radius={size}
/>
</div>);
<ThreeDots color={colors.bgSelected} height={size * 10} width={size * 10} radius={size} />
</div>
);
}

View File

@ -4,30 +4,28 @@ import { CProps } from '../props';
import Overlay from './Overlay';
import TextInput from './TextInput';
interface ConceptSearchProps
extends CProps.Styling {
value: string
onChange?: (newValue: string) => void
noBorder?: boolean
interface ConceptSearchProps extends CProps.Styling {
value: string;
onChange?: (newValue: string) => void;
noBorder?: boolean;
}
function ConceptSearch({ value, onChange, noBorder, ...restProps }: ConceptSearchProps) {
return (
<div {...restProps}>
<Overlay
position='top-[-0.125rem] left-3 translate-y-1/2'
className='pointer-events-none clr-text-controls'
>
<Overlay position='top-[-0.125rem] left-3 translate-y-1/2' className='pointer-events-none clr-text-controls'>
<BiSearchAlt2 size='1.25rem' />
</Overlay>
<TextInput noOutline
<TextInput
noOutline
placeholder='Поиск'
className='pl-10'
noBorder={noBorder}
value={value}
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
/>
</div>);
</div>
);
}
export default ConceptSearch;

View File

@ -4,15 +4,11 @@ import { Tab } from 'react-tabs';
import { globalIDs } from '@/utils/constants';
interface ConceptTabProps
extends Omit<TabProps, 'children'> {
label?: string
interface ConceptTabProps extends Omit<TabProps, 'children'> {
label?: string;
}
function ConceptTab({
label, title, className,
...otherProps
}: ConceptTabProps) {
function ConceptTab({ label, title, className, ...otherProps }: ConceptTabProps) {
return (
<Tab
className={clsx(
@ -23,12 +19,13 @@ function ConceptTab({
'select-none hover:cursor-pointer',
className
)}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
data-tooltip-id={title ? globalIDs.tooltip : undefined}
data-tooltip-content={title}
{...otherProps}
>
{label}
</Tab>);
</Tab>
);
}
ConceptTab.tabsRole = 'Tab';

View File

@ -7,16 +7,16 @@ import { ITooltip, Tooltip } from 'react-tooltip';
import { useConceptTheme } from '@/context/ThemeContext';
interface ConceptTooltipProps
extends Omit<ITooltip, 'variant'> {
layer?: string
text?: string
interface ConceptTooltipProps extends Omit<ITooltip, 'variant'> {
layer?: string;
text?: string;
}
function ConceptTooltip({
text, children,
layer='z-tooltip',
place='bottom',
text,
children,
layer = 'z-tooltip',
place = 'bottom',
className,
style,
...restProps
@ -26,25 +26,22 @@ function ConceptTooltip({
return null;
}
return createPortal(
(<Tooltip
<Tooltip
delayShow={1000}
delayHide={100}
opacity={0.97}
className={clsx(
'overflow-hidden',
'border shadow-md',
layer,
className
)}
className={clsx('overflow-hidden', 'border shadow-md', layer, className)}
classNameArrow={layer}
style={{...{ paddingTop: '2px', paddingBottom: '2px'}, ...style}}
variant={(darkMode ? 'dark' : 'light')}
style={{ ...{ paddingTop: '2px', paddingBottom: '2px' }, ...style }}
variant={darkMode ? 'dark' : 'light'}
place={place}
{...restProps}
>
{text ? text : null}
{children as ReactNode}
</Tooltip>), document.body);
</Tooltip>,
document.body
);
}
export default ConceptTooltip;

View File

@ -1,19 +1,19 @@
import clsx from 'clsx';
interface DividerProps {
vertical?: boolean
margins?: string
vertical?: boolean;
margins?: string;
}
function Divider({ vertical, margins = 'mx-2' }: DividerProps) {
return (
<div className={clsx(
margins,
{
<div
className={clsx(margins, {
'border-x': vertical,
'border-y': !vertical
}
)}/>);
})}
/>
);
}
export default Divider;

View File

@ -5,19 +5,13 @@ import { animateDropdown } from '@/utils/animations';
import { CProps } from '../props';
interface DropdownProps
extends CProps.Styling {
stretchLeft?: boolean
isOpen: boolean
children: React.ReactNode
interface DropdownProps extends CProps.Styling {
stretchLeft?: boolean;
isOpen: boolean;
children: React.ReactNode;
}
function Dropdown({
isOpen, stretchLeft,
className,
children,
...restProps
}: DropdownProps) {
function Dropdown({ isOpen, stretchLeft, className, children, ...restProps }: DropdownProps) {
return (
<div className='relative'>
<motion.div
@ -41,7 +35,8 @@ function Dropdown({
>
{children}
</motion.div>
</div>);
</div>
);
}
export default Dropdown;

View File

@ -6,23 +6,17 @@ import { globalIDs } from '@/utils/constants';
import { CProps } from '../props';
interface DropdownButtonProps
extends CProps.AnimatedButton {
text?: string
icon?: React.ReactNode
interface DropdownButtonProps extends CProps.AnimatedButton {
text?: string;
icon?: React.ReactNode;
children?: React.ReactNode
children?: React.ReactNode;
}
function DropdownButton({
text, icon,
className, title,
onClick,
children,
...restProps
}: DropdownButtonProps) {
function DropdownButton({ text, icon, className, title, onClick, children, ...restProps }: DropdownButtonProps) {
return (
<motion.button type='button'
<motion.button
type='button'
onClick={onClick}
className={clsx(
'px-3 py-1 inline-flex items-center gap-2',
@ -36,14 +30,15 @@ function DropdownButton({
className
)}
variants={animateDropdownItem}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
data-tooltip-id={title ? globalIDs.tooltip : undefined}
data-tooltip-content={title}
{...restProps}
>
{children ? children : null}
{!children && icon ? icon : null}
{!children && text ? <span>{text}</span> : null}
</motion.button>);
</motion.button>
);
}
export default DropdownButton;

View File

@ -6,11 +6,11 @@ import { animateDropdownItem } from '@/utils/animations';
import Checkbox from './Checkbox';
interface DropdownCheckboxProps {
value: boolean
label?: string
title?: string
disabled?: boolean
setValue?: (newValue: boolean) => void
value: boolean;
label?: string;
title?: string;
disabled?: boolean;
setValue?: (newValue: boolean) => void;
}
function DropdownCheckbox({ title, setValue, disabled, ...restProps }: DropdownCheckboxProps) {
@ -25,12 +25,9 @@ function DropdownCheckbox({ title, setValue, disabled, ...restProps }: DropdownC
!!setValue && !disabled && 'clr-hover'
)}
>
<Checkbox
disabled={disabled}
setValue={setValue}
{...restProps}
/>
</motion.div>);
<Checkbox disabled={disabled} setValue={setValue} {...restProps} />
</motion.div>
);
}
export default DropdownCheckbox;

View File

@ -1,12 +1,12 @@
interface EmbedYoutubeProps {
videoID: string
pxHeight: number
pxWidth?: number
videoID: string;
pxHeight: number;
pxWidth?: number;
}
function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps) {
if (!pxWidth) {
pxWidth = pxHeight * 16 / 9;
pxWidth = (pxHeight * 16) / 9;
}
return (
<div
@ -17,7 +17,8 @@ function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps) {
paddingLeft: `${pxWidth}px`
}}
>
<iframe allowFullScreen
<iframe
allowFullScreen
title='Встроенное видео Youtube'
allow='accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
className='absolute top-0 left-0 border'
@ -29,7 +30,8 @@ function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps) {
height={`${pxHeight}px`}
src={`https://www.youtube.com/embed/${videoID}`}
/>
</div>);
</div>
);
}
export default EmbedYoutube;

View File

@ -8,20 +8,14 @@ import { CProps } from '../props';
import Button from './Button';
import Label from './Label';
interface FileInputProps
extends Omit<CProps.Input, 'accept' | 'type'> {
label: string
interface FileInputProps extends Omit<CProps.Input, 'accept' | 'type'> {
label: string;
acceptType?: string
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
acceptType?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
function FileInput({
label, acceptType, title,
className, style,
onChange,
...restProps
}: FileInputProps) {
function FileInput({ label, acceptType, title, className, style, onChange, ...restProps }: FileInputProps) {
const inputRef = useRef<HTMLInputElement | null>(null);
const [fileName, setFileName] = useState('');
@ -41,29 +35,19 @@ function FileInput({
};
return (
<div
className={clsx(
'py-2',
'flex flex-col gap-2 items-center',
className
)}
style={style}
>
<input type='file'
<div className={clsx('py-2', 'flex flex-col gap-2 items-center', className)} style={style}>
<input
type='file'
ref={inputRef}
style={{ display: 'none' }}
accept={acceptType}
onChange={handleFileChange}
{...restProps}
/>
<Button
text={label}
icon={<BiUpload size='1.5rem' />}
onClick={handleUploadClick}
title={title}
/>
<Button text={label} icon={<BiUpload size='1.5rem' />} onClick={handleUploadClick} title={title} />
<Label text={fileName} />
</div>);
</div>
);
}
export default FileInput;

View File

@ -4,23 +4,14 @@ import { classnames } from '@/utils/constants';
import { CProps } from '../props';
export interface FlexColumnProps
extends CProps.Div {}
export interface FlexColumnProps extends CProps.Div {}
function FlexColumn({
className, children,
...restProps
}: FlexColumnProps) {
function FlexColumn({ className, children, ...restProps }: FlexColumnProps) {
return (
<div
className={clsx(
classnames.flex_col,
className
)}
{...restProps}
>
<div className={clsx(classnames.flex_col, className)} {...restProps}>
{children}
</div>);
</div>
);
}
export default FlexColumn;

View File

@ -3,10 +3,6 @@
import { GraphCanvas as GraphUI } from 'reagraph';
export {
type GraphEdge, type GraphNode, type GraphCanvasRef,
type LayoutTypes,
Sphere, useSelection
} from 'reagraph';
export { type GraphEdge, type GraphNode, type GraphCanvasRef, type LayoutTypes, Sphere, useSelection } from 'reagraph';
export default GraphUI;

View File

@ -2,9 +2,8 @@ import clsx from 'clsx';
import { CProps } from '../props';
interface LabelProps
extends CProps.Label {
text?: string
interface LabelProps extends CProps.Label {
text?: string;
}
function Label({ text, className, ...restProps }: LabelProps) {
@ -12,15 +11,10 @@ function Label({ text, className, ...restProps }: LabelProps) {
return null;
}
return (
<label
className={clsx(
'text-sm font-semibold whitespace-nowrap',
className
)}
{...restProps}
>
<label className={clsx('text-sm font-semibold whitespace-nowrap', className)} {...restProps}>
{text}
</label>);
</label>
);
}
export default Label;

View File

@ -1,24 +1,19 @@
interface LabeledValueProps {
id?: string
label: string
text: string | number
title?: string
id?: string;
label: string;
text: string | number;
title?: string;
}
function LabeledValue({ id, label, text, title }: LabeledValueProps) {
return (
<div className='flex justify-between gap-3'>
<label
className='font-semibold'
title={title}
htmlFor={id}
>
<label className='font-semibold' title={title} htmlFor={id}>
{label}
</label>
<span id={id}>
{text}
</span>
</div>);
<span id={id}>{text}</span>
</div>
);
}
export default LabeledValue;

View File

@ -4,19 +4,15 @@ import { globalIDs } from '@/utils/constants';
import { CProps } from '../props';
interface MiniButtonProps
extends CProps.Button {
icon: React.ReactNode
noHover?: boolean
interface MiniButtonProps extends CProps.Button {
icon: React.ReactNode;
noHover?: boolean;
}
function MiniButton({
icon, noHover, tabIndex,
title, className,
...restProps
}: MiniButtonProps) {
function MiniButton({ icon, noHover, tabIndex, title, className, ...restProps }: MiniButtonProps) {
return (
<button type='button'
<button
type='button'
tabIndex={tabIndex ?? -1}
className={clsx(
'px-1 py-1',
@ -29,12 +25,13 @@ function MiniButton({
},
className
)}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
data-tooltip-id={title ? globalIDs.tooltip : undefined}
data-tooltip-content={title}
{...restProps}
>
{icon}
</button>);
</button>
);
}
export default MiniButton;

View File

@ -13,26 +13,30 @@ import Button from './Button';
import MiniButton from './MiniButton';
import Overlay from './Overlay';
export interface ModalProps
extends CProps.Styling {
header?: string
submitText?: string
submitInvalidTooltip?: string
export interface ModalProps extends CProps.Styling {
header?: string;
submitText?: string;
submitInvalidTooltip?: string;
readonly?: boolean
canSubmit?: boolean
readonly?: boolean;
canSubmit?: boolean;
hideWindow: () => void
onSubmit?: () => void
onCancel?: () => void
hideWindow: () => void;
onSubmit?: () => void;
onCancel?: () => void;
children: React.ReactNode
children: React.ReactNode;
}
function Modal({
header, hideWindow, onSubmit,
readonly, onCancel, canSubmit,
submitInvalidTooltip, className,
header,
hideWindow,
onSubmit,
readonly,
onCancel,
canSubmit,
submitInvalidTooltip,
className,
children,
submitText = 'Продолжить',
...restProps
@ -52,60 +56,48 @@ function Modal({
return (
<>
<div className={clsx(
'z-navigation',
'fixed top-0 left-0',
'w-full h-full',
'clr-modal-backdrop'
)}/>
<motion.div ref={ref}
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'clr-modal-backdrop')} />
<motion.div
ref={ref}
className={clsx(
'z-modal',
'fixed bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
'border shadow-md',
'clr-app'
)}
initial={{...animateModal.initial}}
animate={{...animateModal.animate}}
exit={{...animateModal.exit}}
initial={{ ...animateModal.initial }}
animate={{ ...animateModal.animate }}
exit={{ ...animateModal.exit }}
{...restProps}
>
<Overlay position='right-[0.3rem] top-2'>
<MiniButton
title='Закрыть диалоговое окно [ESC]'
icon={<BiX size='1.25rem'/>}
onClick={handleCancel}
/>
<MiniButton title='Закрыть диалоговое окно [ESC]' icon={<BiX size='1.25rem' />} onClick={handleCancel} />
</Overlay>
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
<div
className={clsx(
'overflow-auto',
className
)}
className={clsx('overflow-auto', className)}
style={{
maxHeight: 'calc(100vh - 8rem)',
maxWidth: 'calc(100vw - 2rem)',
maxWidth: 'calc(100vw - 2rem)'
}}
>
{children}
</div>
<div className={clsx(
'z-modal-controls',
'px-6 py-3 flex gap-12 justify-center'
)}>
{!readonly ?
<Button autoFocus
<div className={clsx('z-modal-controls', 'px-6 py-3 flex gap-12 justify-center')}>
{!readonly ? (
<Button
autoFocus
text={submitText}
title={!canSubmit ? submitInvalidTooltip: ''}
title={!canSubmit ? submitInvalidTooltip : ''}
className='min-w-[8rem] min-h-[2.6rem]'
colors='clr-btn-primary'
disabled={!canSubmit}
onClick={handleSubmit}
/> : null}
/>
) : null}
<Button
text={readonly ? 'Закрыть' : 'Отмена'}
className='min-w-[8rem] min-h-[2.6rem]'
@ -113,7 +105,8 @@ function Modal({
/>
</div>
</motion.div>
</>);
</>
);
}
export default Modal;

View File

@ -1,34 +1,22 @@
import clsx from 'clsx'
import clsx from 'clsx';
import { CProps } from '../props'
import { CProps } from '../props';
interface OverlayProps extends CProps.Styling {
id?: string
children: React.ReactNode
position?: string
layer?: string
id?: string;
children: React.ReactNode;
position?: string;
layer?: string;
}
function Overlay({
children, className,
position='top-0 right-0',
layer='z-pop',
...restProps
}: OverlayProps) {
function Overlay({ children, className, position = 'top-0 right-0', layer = 'z-pop', ...restProps }: OverlayProps) {
return (
<div className='relative'>
<div
className={clsx(
'absolute',
className,
position,
layer
)}
{...restProps}
>
<div className={clsx('absolute', className, position, layer)} {...restProps}>
{children}
</div>
</div>);
</div>
);
}
export default Overlay;

View File

@ -1,71 +0,0 @@
'use client';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import { useMemo, useState } from 'react';
import { Document, Page } from 'react-pdf';
import useWindowSize from '@/hooks/useWindowSize';
import { graphLightT } from '@/utils/color';
import Overlay from './Overlay';
import PageControls from './PageControls';
const MAXIMUM_WIDTH = 1000;
const MINIMUM_WIDTH = 600;
interface PDFViewerProps {
file?: string | ArrayBuffer | Blob
}
function PDFViewer({ file }: PDFViewerProps) {
const windowSize = useWindowSize();
const [pageCount, setPageCount] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
const pageWidth = useMemo(
() => {
return Math.max(MINIMUM_WIDTH, (Math.min((windowSize?.width ?? 0) - 300, MAXIMUM_WIDTH)));
}, [windowSize]);
function onDocumentLoadSuccess({ numPages }: PDFDocumentProxy) {
setPageCount(numPages);
}
return (
<Document
file={file}
onLoadSuccess={onDocumentLoadSuccess}
className='px-3'
loading='Загрузка PDF файла...'
error='Не удалось загрузить файл.'
>
<Overlay
position='top-6 left-1/2 -translate-x-1/2'
className='flex select-none'
>
<PageControls
pageCount={pageCount}
pageNumber={pageNumber}
setPageNumber={setPageNumber}
/>
</Overlay>
<Page
className='pointer-events-none select-none'
renderTextLayer={false}
renderAnnotationLayer={false}
pageNumber={pageNumber}
width={pageWidth}
canvasBackground={graphLightT.canvas.background}
/>
<Overlay position='bottom-6 left-1/2 -translate-x-1/2' className='flex select-none'>
<PageControls
pageCount={pageCount}
pageNumber={pageNumber}
setPageNumber={setPageNumber}
/>
</Overlay>
</Document>);
}
export default PDFViewer;

View File

@ -1,44 +0,0 @@
import { BiChevronLeft, BiChevronRight, BiFirstPage, BiLastPage } from 'react-icons/bi';
interface PageControlsProps {
pageNumber: number
pageCount: number
setPageNumber: React.Dispatch<React.SetStateAction<number>>
}
function PageControls({ pageNumber, pageCount, setPageNumber }: PageControlsProps) {
return (
<>
<button type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(1)}
disabled={pageNumber < 2}
>
<BiFirstPage size='1.5rem' />
</button>
<button type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(prev => prev - 1)}
disabled={pageNumber < 2}
>
<BiChevronLeft size='1.5rem' />
</button>
<p className='px-3 text-black'>Страница {pageNumber} из {pageCount}</p>
<button type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(prev => prev + 1)}
disabled={pageNumber >= pageCount}
>
<BiChevronRight size='1.5rem' />
</button>
<button type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(pageCount)}
disabled={pageNumber >= pageCount}
>
<BiLastPage size='1.5rem' />
</button>
</>);
}
export default PageControls;

View File

@ -1,12 +1,9 @@
interface PrettyJsonProps {
data: unknown
data: unknown;
}
function PrettyJson({ data }: PrettyJsonProps) {
return (
<pre>
{JSON.stringify(data, null, 2)}
</pre>);
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
export default PrettyJson;

View File

@ -6,21 +6,17 @@ import Select, { GroupBase, Props, StylesConfig } from 'react-select';
import { useConceptTheme } from '@/context/ThemeContext';
import { selectDarkT, selectLightT } from '@/utils/color';
export interface SelectMultiProps<
Option,
Group extends GroupBase<Option> = GroupBase<Option>
>
extends Omit<Props<Option, true, Group>, 'theme' | 'menuPortalTarget'> {
noPortal?: boolean
export interface SelectMultiProps<Option, Group extends GroupBase<Option> = GroupBase<Option>>
extends Omit<Props<Option, true, Group>, 'theme' | 'menuPortalTarget'> {
noPortal?: boolean;
}
function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>> ({
noPortal, ...restProps
function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
noPortal,
...restProps
}: SelectMultiProps<Option, Group>) {
const { darkMode, colors } = useConceptTheme();
const themeColors = useMemo(
() => !darkMode ? selectLightT : selectDarkT
, [darkMode]);
const themeColors = useMemo(() => (!darkMode ? selectLightT : selectDarkT), [darkMode]);
const adjustedStyles: StylesConfig<Option, true, Group> = useMemo(
() => ({
@ -37,25 +33,28 @@ function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>
borderWidth: '1px',
borderColor: colors.border
}),
menuPortal: (styles) => ({
menuPortal: styles => ({
...styles,
zIndex: 9999
}),
menuList: (styles) => ({
menuList: styles => ({
...styles,
padding: '0px'
}),
input: (styles) => ({...styles}),
placeholder: (styles) => ({...styles}),
multiValue: (styles) => ({
input: styles => ({ ...styles }),
placeholder: styles => ({ ...styles }),
multiValue: styles => ({
...styles,
borderRadius: '0.5rem',
backgroundColor: colors.bgSelected,
backgroundColor: colors.bgSelected
})
}), [colors]);
}),
[colors]
);
return (
<Select isMulti
<Select
isMulti
noOptionsMessage={() => 'Список пуст'}
theme={theme => ({
...theme,
@ -63,12 +62,13 @@ function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>
colors: {
...theme.colors,
...themeColors
},
}
})}
menuPortalTarget={!noPortal ? document.body : null}
styles={adjustedStyles}
{...restProps}
/>);
/>
);
}
export default SelectMulti;

View File

@ -6,21 +6,17 @@ import Select, { GroupBase, Props, StylesConfig } from 'react-select';
import { useConceptTheme } from '@/context/ThemeContext';
import { selectDarkT, selectLightT } from '@/utils/color';
interface SelectSingleProps<
Option,
Group extends GroupBase<Option> = GroupBase<Option>
>
extends Omit<Props<Option, false, Group>, 'theme' | 'menuPortalTarget'> {
noPortal?: boolean
interface SelectSingleProps<Option, Group extends GroupBase<Option> = GroupBase<Option>>
extends Omit<Props<Option, false, Group>, 'theme' | 'menuPortalTarget'> {
noPortal?: boolean;
}
function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option>> ({
noPortal, ...restProps
function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
noPortal,
...restProps
}: SelectSingleProps<Option, Group>) {
const { darkMode, colors } = useConceptTheme();
const themeColors = useMemo(
() => !darkMode ? selectLightT : selectDarkT
, [darkMode]);
const themeColors = useMemo(() => (!darkMode ? selectLightT : selectDarkT), [darkMode]);
const adjustedStyles: StylesConfig<Option, false, Group> = useMemo(
() => ({
@ -30,11 +26,11 @@ function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option
cursor: isDisabled ? 'not-allowed' : 'pointer',
boxShadow: 'none'
}),
menuPortal: (styles) => ({
menuPortal: styles => ({
...styles,
zIndex: 9999
}),
menuList: (styles) => ({
menuList: styles => ({
...styles,
padding: '0px'
}),
@ -45,10 +41,12 @@ function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option
borderWidth: '1px',
borderColor: colors.border
}),
input: (styles) => ({...styles}),
placeholder: (styles) => ({...styles}),
singleValue: (styles) => ({...styles}),
}), [colors]);
input: styles => ({ ...styles }),
placeholder: styles => ({ ...styles }),
singleValue: styles => ({ ...styles })
}),
[colors]
);
return (
<Select
@ -59,12 +57,13 @@ function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option
colors: {
...theme.colors,
...themeColors
},
}
})}
menuPortalTarget={!noPortal ? document.body : null}
styles={adjustedStyles}
{...restProps}
/>);
/>
);
}
export default SelectSingle;

View File

@ -4,25 +4,27 @@ import { globalIDs } from '@/utils/constants';
import { CProps } from '../props';
interface SelectorButtonProps
extends CProps.Button {
text?: string
icon?: React.ReactNode
interface SelectorButtonProps extends CProps.Button {
text?: string;
icon?: React.ReactNode;
colors?: string
transparent?: boolean
colors?: string;
transparent?: boolean;
}
function SelectorButton({
text, icon, title,
text,
icon,
title,
colors = 'clr-btn-default',
className,
transparent,
...restProps
}: SelectorButtonProps) {
return (
<button type='button'
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
<button
type='button'
data-tooltip-id={title ? globalIDs.tooltip : undefined}
data-tooltip-content={title}
className={clsx(
'px-1 flex flex-start items-center gap-1',
@ -31,7 +33,7 @@ function SelectorButton({
'disabled:cursor-not-allowed cursor-pointer',
{
'clr-hover': transparent,
'border': !transparent,
'border': !transparent
},
className,
!transparent && colors
@ -40,7 +42,8 @@ function SelectorButton({
>
{icon ? icon : null}
{text ? <div className={'font-semibold whitespace-nowrap pb-1'}>{text}</div> : null}
</button>);
</button>
);
}
export default SelectorButton;

View File

@ -2,21 +2,16 @@ import clsx from 'clsx';
import { CProps } from '../props';
interface SubmitButtonProps
extends CProps.Button {
text?: string
loading?: boolean
icon?: React.ReactNode
interface SubmitButtonProps extends CProps.Button {
text?: string;
loading?: boolean;
icon?: React.ReactNode;
}
function SubmitButton({
text = 'ОК',
icon, disabled, loading,
className,
...restProps
}: SubmitButtonProps) {
function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...restProps }: SubmitButtonProps) {
return (
<button type='submit'
<button
type='submit'
className={clsx(
'px-3 py-2 flex gap-2 items-center justify-center',
'border',
@ -31,7 +26,8 @@ function SubmitButton({
>
{icon ? <span>{icon}</span> : null}
{text ? <span>{text}</span> : null}
</button>);
</button>
);
}
export default SubmitButton;

View File

@ -2,24 +2,30 @@ 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
interface SwitchButtonProps<ValueType> extends CProps.Styling {
id?: string;
value: ValueType;
label?: string;
icon?: React.ReactNode;
title?: string;
isSelected?: boolean
onSelect: (value: ValueType) => void
isSelected?: boolean;
onSelect: (value: ValueType) => void;
}
function SwitchButton<ValueType>({
value, icon, label, className,
isSelected, onSelect, ...restProps
value,
icon,
label,
className,
isSelected,
onSelect,
...restProps
}: SwitchButtonProps<ValueType>) {
return (
<button type='button' tabIndex={-1}
<button
type='button'
tabIndex={-1}
onClick={() => onSelect(value)}
className={clsx(
'px-2 py-1',
@ -34,7 +40,8 @@ function SwitchButton<ValueType>({
>
{icon ? icon : null}
{label}
</button>);
</button>
);
}
export default SwitchButton;

View File

@ -3,28 +3,35 @@ import clsx from 'clsx';
import { CProps } from '../props';
import Label from './Label';
export interface TextAreaProps
extends CProps.Editor, CProps.Colors, CProps.TextArea {
dense?: boolean
export interface TextAreaProps extends CProps.Editor, CProps.Colors, CProps.TextArea {
dense?: boolean;
}
function TextArea({
id, label, required, rows,
dense, noBorder, noOutline,
id,
label,
required,
rows,
dense,
noBorder,
noOutline,
className,
colors = 'clr-input',
...restProps
}: TextAreaProps) {
return (
<div className={clsx(
<div
className={clsx(
{
'flex flex-col gap-2': !dense,
'flex items-center gap-3': dense
},
dense && className,
)}>
dense && className
)}
>
<Label text={label} htmlFor={id} />
<textarea id={id}
<textarea
id={id}
className={clsx(
'px-3 py-2',
'leading-tight',
@ -40,7 +47,8 @@ function TextArea({
required={required}
{...restProps}
/>
</div>);
</div>
);
}
export default TextArea;

View File

@ -3,10 +3,9 @@ import clsx from 'clsx';
import { CProps } from '../props';
import Label from './Label';
interface TextInputProps
extends CProps.Editor, CProps.Colors, CProps.Input {
dense?: boolean
allowEnter?: boolean
interface TextInputProps extends CProps.Editor, CProps.Colors, CProps.Input {
dense?: boolean;
allowEnter?: boolean;
}
function preventEnterCapture(event: React.KeyboardEvent<HTMLInputElement>) {
@ -16,23 +15,31 @@ function preventEnterCapture(event: React.KeyboardEvent<HTMLInputElement>) {
}
function TextInput({
id, label,
dense, noBorder, noOutline, allowEnter, disabled,
id,
label,
dense,
noBorder,
noOutline,
allowEnter,
disabled,
className,
colors = 'clr-input',
onKeyDown,
...restProps
}: TextInputProps) {
return (
<div className={clsx(
<div
className={clsx(
{
'flex flex-col gap-2': !dense,
'flex items-center gap-3': dense,
'flex items-center gap-3': dense
},
dense && className
)}>
)}
>
<Label text={label} htmlFor={id} />
<input id={id}
<input
id={id}
className={clsx(
'py-2',
'leading-tight truncate hover:text-clip',
@ -49,7 +56,8 @@ function TextInput({
disabled={disabled}
{...restProps}
/>
</div>);
</div>
);
}
export default TextInput;

View File

@ -1,34 +1,27 @@
import { Link } from 'react-router-dom';
interface TextURLProps {
text: string
title?: string
href?: string
color?: string
onClick?: () => void
text: string;
title?: string;
href?: string;
color?: string;
onClick?: () => void;
}
function TextURL({ text, href, title, color='clr-text-url', onClick }: TextURLProps) {
function TextURL({ text, href, title, color = 'clr-text-url', onClick }: TextURLProps) {
const design = `cursor-pointer hover:underline ${color}`;
if (href) {
return (
<Link tabIndex={-1}
className={design}
title={title}
to={href}
>
<Link tabIndex={-1} className={design} title={title} to={href}>
{text}
</Link>
);
} else if (onClick) {
return (
<span tabIndex={-1}
className={design}
onClick={onClick}
>
<span tabIndex={-1} className={design} onClick={onClick}>
{text}
</span>);
</span>
);
} else {
return null;
}

View File

@ -1,4 +1,3 @@
import clsx from 'clsx';
import { useMemo } from 'react';
@ -8,26 +7,19 @@ import { CheckboxCheckedIcon, CheckboxNullIcon } from '../Icons';
import { CheckboxProps } from './Checkbox';
import Label from './Label';
export interface TristateProps
extends Omit<CheckboxProps, 'value' | 'setValue'> {
value: boolean | null
setValue?: (newValue: boolean | null) => void
export interface TristateProps extends Omit<CheckboxProps, 'value' | 'setValue'> {
value: boolean | null;
setValue?: (newValue: boolean | null) => void;
}
function Tristate({
id, disabled, label, title,
className,
value, setValue,
...restProps
}: TristateProps) {
const cursor = useMemo(
() => {
function Tristate({ id, disabled, label, title, className, value, setValue, ...restProps }: TristateProps) {
const cursor = useMemo(() => {
if (disabled) {
return 'cursor-not-allowed';
} else if (setValue) {
return 'cursor-pointer';
} else {
return ''
return '';
}
}, [disabled, setValue]);
@ -46,32 +38,36 @@ function Tristate({
}
return (
<button type='button' id={id}
className={clsx(
'flex items-center gap-2 text-start',
'outline-none',
cursor,
className
)}
<button
type='button'
id={id}
className={clsx('flex items-center gap-2 text-start', 'outline-none', cursor, className)}
disabled={disabled}
onClick={handleClick}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
data-tooltip-id={title ? globalIDs.tooltip : undefined}
data-tooltip-content={title}
{...restProps}
>
<div className={clsx(
'w-4 h-4',
'border rounded-sm',
{
<div
className={clsx('w-4 h-4', 'border rounded-sm', {
'clr-primary': value !== false,
'clr-app': value === false
}
)}>
{value ? <div className='mt-[1px] ml-[1px]'><CheckboxCheckedIcon /></div> : null}
{value == null ? <div className='mt-[1px] ml-[1px]'><CheckboxNullIcon /></div> : null}
})}
>
{value ? (
<div className='mt-[1px] ml-[1px]'>
<CheckboxCheckedIcon />
</div>
) : null}
{value == null ? (
<div className='mt-[1px] ml-[1px]'>
<CheckboxNullIcon />
</div>
) : null}
</div>
<Label className={cursor} text={label} htmlFor={id} />
</button>);
</button>
);
}
export default Tristate;

View File

@ -2,16 +2,12 @@ import { ToastContainer, type ToastContainerProps } from 'react-toastify';
import { useConceptTheme } from '@/context/ThemeContext';
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'>{}
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {}
function ToasterThemed(props: ToasterThemedProps) {
const { darkMode } = useConceptTheme();
return (
<ToastContainer
theme={ darkMode ? 'dark' : 'light'}
{...props}
/>);
return <ToastContainer theme={darkMode ? 'dark' : 'light'} {...props} />;
}
export default ToasterThemed;

View File

@ -2,10 +2,17 @@
import {
ColumnSort,
createColumnHelper, getCoreRowModel,
getPaginationRowModel, getSortedRowModel,
PaginationState, RowData, type RowSelectionState,
SortingState, TableOptions, useReactTable, type VisibilityState
createColumnHelper,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
PaginationState,
RowData,
type RowSelectionState,
SortingState,
TableOptions,
useReactTable,
type VisibilityState
} from '@tanstack/react-table';
import clsx from 'clsx';
import { useState } from 'react';
@ -20,39 +27,37 @@ import TableHeader from './TableHeader';
export { createColumnHelper, type ColumnSort, type RowSelectionState, type VisibilityState };
export interface IConditionalStyle<TData> {
when: (rowData: TData) => boolean
style: React.CSSProperties
when: (rowData: TData) => boolean;
style: React.CSSProperties;
}
export interface DataTableProps<TData extends RowData>
extends CProps.Styling, Pick<TableOptions<TData>,
'data' | 'columns' |
'onRowSelectionChange' | 'onColumnVisibilityChange'
> {
dense?: boolean
headPosition?: string
noHeader?: boolean
noFooter?: boolean
extends CProps.Styling,
Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
dense?: boolean;
headPosition?: string;
noHeader?: boolean;
noFooter?: boolean;
conditionalRowStyles?: IConditionalStyle<TData>[]
noDataComponent?: React.ReactNode
conditionalRowStyles?: IConditionalStyle<TData>[];
noDataComponent?: React.ReactNode;
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void;
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void;
enableRowSelection?: boolean
rowSelection?: RowSelectionState
enableRowSelection?: boolean;
rowSelection?: RowSelectionState;
enableHiding?: boolean
columnVisibility?: VisibilityState
enableHiding?: boolean;
columnVisibility?: VisibilityState;
enablePagination?: boolean
paginationPerPage?: number
paginationOptions?: number[]
onChangePaginationOption?: (newValue: number) => void
enablePagination?: boolean;
paginationPerPage?: number;
paginationOptions?: number[];
onChangePaginationOption?: (newValue: number) => void;
enableSorting?: boolean
initialSorting?: ColumnSort
enableSorting?: boolean;
initialSorting?: ColumnSort;
}
/**
@ -60,11 +65,18 @@ extends CProps.Styling, Pick<TableOptions<TData>,
*
* @param headPosition - Top position of sticky header (0 if no other sticky elements are present).
* No sticky header if omitted
*/
*/
function DataTable<TData extends RowData>({
style, className,
dense, headPosition, conditionalRowStyles, noFooter, noHeader,
onRowClicked, onRowDoubleClicked, noDataComponent,
style,
className,
dense,
headPosition,
conditionalRowStyles,
noFooter,
noHeader,
onRowClicked,
onRowDoubleClicked,
noDataComponent,
enableRowSelection,
rowSelection,
@ -76,8 +88,8 @@ function DataTable<TData extends RowData>({
initialSorting,
enablePagination,
paginationPerPage=10,
paginationOptions=[10, 20, 30, 40, 50],
paginationPerPage = 10,
paginationOptions = [10, 20, 30, 40, 50],
onChangePaginationOption,
...restProps
@ -86,7 +98,7 @@ function DataTable<TData extends RowData>({
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: paginationPerPage,
pageSize: paginationPerPage
});
const tableImpl = useReactTable({
@ -112,13 +124,14 @@ function DataTable<TData extends RowData>({
return (
<div className={clsx(className)} style={style}>
<table className='w-full'>
{!noHeader ?
{!noHeader ? (
<TableHeader
table={tableImpl}
enableRowSelection={enableRowSelection}
enableSorting={enableSorting}
headPosition={headPosition}
/>: null}
/>
) : null}
<TableBody
table={tableImpl}
@ -129,20 +142,19 @@ function DataTable<TData extends RowData>({
onRowDoubleClicked={onRowDoubleClicked}
/>
{!noFooter ?
<TableFooter
table={tableImpl}
/>: null}
{!noFooter ? <TableFooter table={tableImpl} /> : null}
</table>
{(enablePagination && !isEmpty) ?
{enablePagination && !isEmpty ? (
<PaginationTools
table={tableImpl}
paginationOptions={paginationOptions}
onChangePaginationOption={onChangePaginationOption}
/> : null}
{isEmpty ? (noDataComponent ?? <DefaultNoData />) : null}
</div>);
/>
) : null}
{isEmpty ? noDataComponent ?? <DefaultNoData /> : null}
</div>
);
}
export default DataTable;

View File

@ -1,8 +1,5 @@
function defaultNoDataComponent() {
return (
<div className='p-2 text-center'>
Данные отсутствуют
</div>);
return <div className='p-2 text-center'>Данные отсутствуют</div>;
}
export default defaultNoDataComponent;

View File

@ -8,9 +8,9 @@ import { BiChevronLeft, BiChevronRight, BiFirstPage, BiLastPage } from 'react-ic
import { prefixes } from '@/utils/constants';
interface PaginationToolsProps<TData> {
table: Table<TData>
paginationOptions: number[]
onChangePaginationOption?: (newValue: number) => void
table: Table<TData>;
paginationOptions: number[];
onChangePaginationOption?: (newValue: number) => void;
}
function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOption }: PaginationToolsProps<TData>) {
@ -21,32 +21,33 @@ function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOp
if (onChangePaginationOption) {
onChangePaginationOption(perPage);
}
}, [onChangePaginationOption, table]);
},
[onChangePaginationOption, table]
);
return (
<div className={clsx(
'flex justify-end items-center',
'my-2',
'text-sm',
'clr-text-controls',
'select-none'
)}>
<div className={clsx('flex justify-end items-center', 'my-2', 'text-sm', 'clr-text-controls', 'select-none')}>
<span className='mr-3'>
{`${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
-
${Math.min(table.getFilteredRowModel().rows.length, (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize)}
${Math.min(
table.getFilteredRowModel().rows.length,
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize
)}
из
${table.getFilteredRowModel().rows.length}`}
</span>
<div className='flex'>
<button type='button'
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<BiFirstPage size='1.5rem' />
</button>
<button type='button'
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
@ -64,14 +65,16 @@ function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOp
}
}}
/>
<button type='button'
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<BiChevronRight size='1.5rem' />
</button>
<button type='button'
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
@ -84,14 +87,14 @@ function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOp
onChange={handlePaginationOptionsChange}
className='mx-2 cursor-pointer clr-app'
>
{paginationOptions.map(
(pageSize) => (
{paginationOptions.map(pageSize => (
<option key={`${prefixes.page_size}${pageSize}`} value={pageSize}>
{pageSize} на стр
</option>
))}
</select>
</div>);
</div>
);
}
export default PaginationTools;

View File

@ -3,20 +3,20 @@ import { Table } from '@tanstack/react-table';
import Tristate from '@/components/Common/Tristate';
interface SelectAllProps<TData> {
table: Table<TData>
table: Table<TData>;
}
function SelectAll<TData>({ table }: SelectAllProps<TData>) {
return (
<Tristate tabIndex={-1}
<Tristate
tabIndex={-1}
title='Выделить все'
value={
(!table.getIsAllPageRowsSelected() && table.getIsSomePageRowsSelected())
? null
: table.getIsAllPageRowsSelected()
!table.getIsAllPageRowsSelected() && table.getIsSomePageRowsSelected() ? null : table.getIsAllPageRowsSelected()
}
setValue={value => table.toggleAllPageRowsSelected(value !== false)}
/>);
/>
);
}
export default SelectAll;

View File

@ -3,15 +3,11 @@ import { Row } from '@tanstack/react-table';
import Checkbox from '@/components/Common/Checkbox';
interface SelectRowProps<TData> {
row: Row<TData>
row: Row<TData>;
}
function SelectRow<TData>({ row }: SelectRowProps<TData>) {
return (
<Checkbox tabIndex={-1}
value={row.getIsSelected()}
setValue={row.getToggleSelectedHandler()}
/>);
return <Checkbox tabIndex={-1} value={row.getIsSelected()} setValue={row.getToggleSelectedHandler()} />;
}
export default SelectRow;

View File

@ -2,18 +2,18 @@ import { Column } from '@tanstack/react-table';
import { BiCaretDown, BiCaretUp } from 'react-icons/bi';
interface SortingIconProps<TData> {
column: Column<TData>
column: Column<TData>;
}
function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
return (<>
return (
<>
{{
desc: <BiCaretDown size='1rem' />,
asc: <BiCaretUp size='1rem'/>,
}[column.getIsSorted() as string] ??
<BiCaretDown size='1rem' className='opacity-0 hover:opacity-50' />
}
</>);
asc: <BiCaretUp size='1rem' />
}[column.getIsSorted() as string] ?? <BiCaretDown size='1rem' className='opacity-0 hover:opacity-50' />}
</>
);
}
export default SortingIcon;

View File

@ -4,19 +4,21 @@ import { IConditionalStyle } from '.';
import SelectRow from './SelectRow';
interface TableBodyProps<TData> {
table: Table<TData>
dense?: boolean
enableRowSelection?: boolean
conditionalRowStyles?: IConditionalStyle<TData>[]
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
table: Table<TData>;
dense?: boolean;
enableRowSelection?: boolean;
conditionalRowStyles?: IConditionalStyle<TData>[];
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void;
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void;
}
function TableBody<TData>({
table, dense,
table,
dense,
enableRowSelection,
conditionalRowStyles,
onRowClicked, onRowDoubleClicked
onRowClicked,
onRowDoubleClicked
}: TableBodyProps<TData>) {
function handleRowClicked(row: Row<TData>, event: React.MouseEvent<Element, MouseEvent>) {
if (onRowClicked) {
@ -28,47 +30,51 @@ function TableBody<TData>({
}
function getRowStyles(row: Row<TData>) {
return ({...conditionalRowStyles!
return {
...conditionalRowStyles!
.filter(item => item.when(row.original))
.reduce((prev, item) => ({...prev, ...item.style}), {})
});
.reduce((prev, item) => ({ ...prev, ...item.style }), {})
};
}
return (
<tbody>
{table.getRowModel().rows.map(
(row: Row<TData>, index) => (
{table.getRowModel().rows.map((row: Row<TData>, index) => (
<tr
key={row.id}
className={
row.getIsSelected() ? 'clr-selected clr-hover' :
index % 2 === 0 ? 'clr-controls clr-hover' : 'clr-app clr-hover'
row.getIsSelected()
? 'clr-selected clr-hover'
: index % 2 === 0
? 'clr-controls clr-hover'
: 'clr-app clr-hover'
}
style={conditionalRowStyles && getRowStyles(row)}
>
{enableRowSelection ?
{enableRowSelection ? (
<td key={`select-${row.id}`} className='pl-3 pr-1 border-y'>
<SelectRow row={row} />
</td> : null}
{row.getVisibleCells().map(
(cell: Cell<TData, unknown>) => (
</td>
) : null}
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
<td
key={cell.id}
className='px-2 border-y'
style={{
cursor: onRowClicked || onRowDoubleClicked ? 'pointer': 'auto',
paddingBottom: dense ? '0.25rem': '0.5rem',
paddingTop: dense ? '0.25rem': '0.5rem'
cursor: onRowClicked || onRowDoubleClicked ? 'pointer' : 'auto',
paddingBottom: dense ? '0.25rem' : '0.5rem',
paddingTop: dense ? '0.25rem' : '0.5rem'
}}
onClick={event => handleRowClicked(row, event)}
onDoubleClick={event => onRowDoubleClicked ? onRowDoubleClicked(row.original, event) : undefined}
onDoubleClick={event => (onRowDoubleClicked ? onRowDoubleClicked(row.original, event) : undefined)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>);
</tbody>
);
}
export default TableBody;

View File

@ -1,24 +1,23 @@
import { flexRender, Header, HeaderGroup, Table } from '@tanstack/react-table';
interface TableFooterProps<TData> {
table: Table<TData>
table: Table<TData>;
}
function TableFooter<TData>({ table }: TableFooterProps<TData>) {
return (
<tfoot>
{table.getFooterGroups().map(
(footerGroup: HeaderGroup<TData>) => (
{table.getFooterGroups().map((footerGroup: HeaderGroup<TData>) => (
<tr key={footerGroup.id}>
{footerGroup.headers.map(
(header: Header<TData, unknown>) => (
{footerGroup.headers.map((header: Header<TData, unknown>) => (
<th key={header.id}>
{!header.isPlaceholder ? flexRender(header.column.columnDef.footer, header.getContext()) : null}
</th>
))}
</tr>
))}
</tfoot>);
</tfoot>
);
}
export default TableFooter;

View File

@ -4,16 +4,13 @@ import SelectAll from './SelectAll';
import SortingIcon from './SortingIcon';
interface TableHeaderProps<TData> {
table: Table<TData>
headPosition?: string
enableRowSelection?: boolean
enableSorting?: boolean
table: Table<TData>;
headPosition?: string;
enableRowSelection?: boolean;
enableSorting?: boolean;
}
function TableHeader<TData>({
table, headPosition,
enableRowSelection, enableSorting
}: TableHeaderProps<TData>) {
function TableHeader<TData>({ table, headPosition, enableRowSelection, enableSorting }: TableHeaderProps<TData>) {
return (
<thead
className={`clr-app shadow-border`}
@ -22,35 +19,37 @@ function TableHeader<TData>({
position: 'sticky'
}}
>
{table.getHeaderGroups().map(
(headerGroup: HeaderGroup<TData>) => (
{table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
<tr key={headerGroup.id}>
{enableRowSelection ?
{enableRowSelection ? (
<th className='pl-3 pr-1'>
<SelectAll table={table} />
</th> : null}
{headerGroup.headers.map(
(header: Header<TData, unknown>) => (
<th key={header.id}
</th>
) : null}
{headerGroup.headers.map((header: Header<TData, unknown>) => (
<th
key={header.id}
colSpan={header.colSpan}
className='px-2 py-2 text-xs font-semibold select-none whitespace-nowrap'
style={{
textAlign: header.getSize() > 100 ? 'left': 'center',
textAlign: header.getSize() > 100 ? 'left' : 'center',
width: header.getSize(),
cursor: enableSorting && header.column.getCanSort() ? 'pointer': 'auto',
cursor: enableSorting && header.column.getCanSort() ? 'pointer' : 'auto'
}}
onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined}
>
{!header.isPlaceholder ? (
<div className='flex gap-1'>
{flexRender(header.column.columnDef.header, header.getContext())}
{(enableSorting && header.column.getCanSort()) ? <SortingIcon column={header.column} /> : null}
</div>) : null}
{enableSorting && header.column.getCanSort() ? <SortingIcon column={header.column} /> : null}
</div>
) : null}
</th>
))}
</tr>
))}
</thead>);
</thead>
);
}
export default TableHeader;

View File

@ -1,5 +1,7 @@
export {
default,
createColumnHelper, type IConditionalStyle,
type RowSelectionState, type VisibilityState
createColumnHelper,
type IConditionalStyle,
type RowSelectionState,
type VisibilityState
} from './DataTable';

View File

@ -7,12 +7,10 @@ function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div className='flex flex-col items-center antialiased clr-app' role='alert'>
<h1>Что-то пошло не так!</h1>
<Button
onClick={resetErrorBoundary}
text='Попробовать еще раз'
/>
<Button onClick={resetErrorBoundary} text='Попробовать еще раз' />
<InfoError error={error as Error} />
</div>);
</div>
);
}
export default ErrorFallback;

View File

@ -15,20 +15,18 @@ function ExpectedAnonymous() {
<div className='flex flex-col items-center gap-3 py-6'>
<p className='font-semibold'>{`Вы вошли в систему как ${user?.username ?? ''}`}</p>
<div className='flex gap-3'>
<TextURL text='Новая схема' href='/library/create'/>
<TextURL text='Новая схема' href='/library/create' />
<span> | </span>
<TextURL text='Библиотека' href='/library'/>
<TextURL text='Библиотека' href='/library' />
<span> | </span>
<TextURL text='Справка' href='/manuals'/>
<TextURL text='Справка' href='/manuals' />
<span> | </span>
<span
className='cursor-pointer hover:underline clr-text-url'
onClick={logoutAndRedirect}
>
<span className='cursor-pointer hover:underline clr-text-url' onClick={logoutAndRedirect}>
Выйти
</span>
</div>
</div>);
</div>
);
}
export default ExpectedAnonymous;

View File

@ -11,7 +11,8 @@ function Footer() {
return null;
}
return (
<footer tabIndex={-1}
<footer
tabIndex={-1}
className={clsx(
'z-navigation',
'px-4 py-2 flex flex-col items-center gap-1',
@ -19,15 +20,16 @@ function Footer() {
)}
>
<div className='flex gap-3'>
<TextURL text='Библиотека' href='/library' color='clr-footer'/>
<TextURL text='Справка' href='/manuals' color='clr-footer'/>
<TextURL text='Центр Концепт' href={urls.concept} color='clr-footer'/>
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='clr-footer'/>
<TextURL text='Библиотека' href='/library' color='clr-footer' />
<TextURL text='Справка' href='/manuals' color='clr-footer' />
<TextURL text='Центр Концепт' href={urls.concept} color='clr-footer' />
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='clr-footer' />
</div>
<div>
<p className='clr-footer'>© 2024 ЦИВТ КОНЦЕПТ</p>
</div>
</footer>);
</footer>
);
}
export default Footer;

View File

@ -3,18 +3,16 @@ import InfoConstituenta from '@/components/Shared/InfoConstituenta';
import { IConstituenta } from '@/models/rsform';
interface ConstituentaTooltipProps {
data: IConstituenta
anchor: string
data: IConstituenta;
anchor: string;
}
function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
return (
<ConceptTooltip clickable
anchorSelect={anchor}
className='max-w-[30rem]'
>
<ConceptTooltip clickable anchorSelect={anchor} className='max-w-[30rem]'>
<InfoConstituenta data={data} />
</ConceptTooltip>);
</ConceptTooltip>
);
}
export default ConstituentaTooltip;

View File

@ -7,24 +7,16 @@ import { HelpTopic } from '@/models/miscellaneous';
import { CProps } from '../props';
import InfoTopic from './InfoTopic';
interface HelpButtonProps
extends CProps.Styling {
topic: HelpTopic
offset?: number
interface HelpButtonProps extends CProps.Styling {
topic: HelpTopic;
offset?: number;
}
function HelpButton({ topic, ...restProps }: HelpButtonProps) {
return (
<div
id={`help-${topic}`}
className='p-1'
>
<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}
>
<ConceptTooltip 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}`} />
@ -32,7 +24,8 @@ function HelpButton({ topic, ...restProps }: HelpButtonProps) {
</div>
<InfoTopic topic={topic} />
</ConceptTooltip>
</div>);
</div>
);
}
export default HelpButton;

View File

@ -2,6 +2,7 @@ import Divider from '@/components/Common/Divider';
import InfoCstStatus from '@/components/Shared/InfoCstStatus';
function HelpConstituenta() {
// prettier-ignore
return (
<div className='leading-tight'>
<h1>Редактор конституент</h1>

View File

@ -2,6 +2,7 @@ import TextURL from '@/components/Common/TextURL';
import { urls } from '@/utils/constants';
function HelpExteor() {
// prettier-ignore
return (
<div>
<h1>Экстеор</h1>

View File

@ -2,6 +2,7 @@ import { BiCheckShield, BiShareAlt } from 'react-icons/bi';
import { FiBell } from 'react-icons/fi';
function HelpLibrary() {
// prettier-ignore
return (
<div className='max-w-[80rem]'>
<h1>Библиотека концептуальных схем</h1>

View File

@ -2,6 +2,7 @@ import TextURL from '@/components/Common/TextURL';
import { urls } from '@/utils/constants';
function HelpMain() {
// prettier-ignore
return (
<div className='max-w-[60rem]'>
<h1>Портал</h1>

View File

@ -1,11 +1,8 @@
import PDFViewer from '@/components/Common/PDFViewer';
import PDFViewer from '@/components/PDFViewer';
import { resources } from '@/utils/constants';
function HelpPrivacy() {
return (
<PDFViewer
file={resources.privacy_policy}
/>);
return <PDFViewer file={resources.privacy_policy} />;
}
export default HelpPrivacy;

View File

@ -2,6 +2,7 @@ import Divider from '@/components/Common/Divider';
import InfoCstStatus from '@/components/Shared/InfoCstStatus';
function HelpRSFormItems() {
// prettier-ignore
return (
<div>
<h1>Горячие клавиши</h1>

View File

@ -1,4 +1,5 @@
function HelpRSFormMeta() {
// prettier-ignore
return (
<div>
<h1>Паспорт схемы</h1>

View File

@ -9,13 +9,13 @@ const OPT_VIDEO_H = 1080;
function HelpRSLang() {
const windowSize = useWindowSize();
const videoHeight = useMemo(
() => {
const videoHeight = useMemo(() => {
const viewH = windowSize.height ?? 0;
const viewW = windowSize.width ?? 0;
return Math.min(OPT_VIDEO_H, viewH - 320, Math.floor((viewW - 290)*9/16));
return Math.min(OPT_VIDEO_H, viewH - 320, Math.floor(((viewW - 290) * 9) / 16));
}, [windowSize]);
// prettier-ignore
return (
<div className='flex flex-col gap-4'>
<div>

View File

@ -1,4 +1,5 @@
function HelpRSTemplates() {
// prettier-ignore
return (
<div className='flex flex-col gap-2 pb-2 max-w-[80rem]'>
<h1>Банк выражений</h1>

View File

@ -3,6 +3,7 @@ import InfoCstClass from '@/components/Shared/InfoCstClass';
import InfoCstStatus from '@/components/Shared/InfoCstStatus';
function HelpTermGraph() {
// prettier-ignore
return (
<div className='flex max-w-[80rem]'>
<div>

View File

@ -1,5 +1,5 @@
function HelpTerminologyControl() {
// prettier-ignore
return (
<div className='flex flex-col gap-1 max-w-[80rem]'>
<h1>Терминологизация</h1>

View File

@ -14,7 +14,7 @@ import HelpTermGraph from './HelpTermGraph';
import HelpTerminologyControl from './HelpTerminologyControl';
interface InfoTopicProps {
topic: HelpTopic
topic: HelpTopic;
}
function InfoTopic({ topic }: InfoTopicProps) {

View File

@ -1,16 +1,16 @@
// Search new icons at https://reactsvgicons.com/
interface IconSVGProps {
viewBox: string
size?: string
className?: string
props?: React.SVGProps<SVGSVGElement>
children: React.ReactNode
viewBox: string;
size?: string;
className?: string;
props?: React.SVGProps<SVGSVGElement>;
children: React.ReactNode;
}
export interface IconProps {
size?: string
className?: string
size?: string;
className?: string;
}
function IconSVG({ viewBox, size = '1.5rem', className, props, children }: IconSVGProps) {
@ -24,7 +24,8 @@ function IconSVG({ viewBox, size = '1.5rem', className, props, children }: IconS
{...props}
>
{children}
</svg>);
</svg>
);
}
export function EducationIcon(props: IconProps) {
@ -46,11 +47,7 @@ export function InDoorIcon(props: IconProps) {
export function CheckboxCheckedIcon() {
return (
<svg
className='w-3 h-3'
viewBox='0 0 512 512'
fill='#ffffff'
>
<svg className='w-3 h-3' viewBox='0 0 512 512' fill='#ffffff'>
<path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' />
</svg>
);
@ -58,11 +55,7 @@ export function CheckboxCheckedIcon() {
export function CheckboxNullIcon() {
return (
<svg
className='w-3 h-3'
viewBox='0 0 16 16'
fill='#ffffff'
>
<svg className='w-3 h-3' viewBox='0 0 16 16' fill='#ffffff'>
<path d='M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z' />
</svg>
);

View File

@ -7,10 +7,10 @@ import PrettyJson from './Common/PrettyJSON';
export type ErrorData = string | Error | AxiosError | undefined;
interface InfoErrorProps {
error: ErrorData
error: ErrorData;
}
function DescribeError({error} : {error: ErrorData}) {
function DescribeError({ error }: { error: ErrorData }) {
if (!error) {
return <p>Ошибки отсутствуют</p>;
} else if (typeof error === 'string') {
@ -26,7 +26,8 @@ function DescribeError({error} : {error: ErrorData}) {
<div>
<p>{'Обращение к несуществующему API'}</p>
<PrettyJson data={error} />
</div>);
</div>
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
@ -35,11 +36,13 @@ function DescribeError({error} : {error: ErrorData}) {
<div>
<p className='underline'>Ошибка</p>
<p>{error.message}</p>
{error.response.data && (<>
{error.response.data && (
<>
<p className='mt-2 underline'>Описание</p>
{isHtml ? <div dangerouslySetInnerHTML={{ __html: error.response.data as TrustedHTML }} /> : null}
{!isHtml ? <PrettyJson data={error.response.data as object} /> : null}
</>)}
</>
)}
</div>
);
}
@ -48,7 +51,8 @@ function InfoError({ error }: InfoErrorProps) {
return (
<div className='px-3 py-2 min-w-[15rem] text-sm font-semibold select-text clr-text-warning'>
<DescribeError error={error} />
</div>);
</div>
);
}
export default InfoError;

View File

@ -3,9 +3,11 @@ import { useConceptTheme } from '@/context/ThemeContext';
function Logo() {
const { darkMode } = useConceptTheme();
return (
<img alt='Логотип КонцептПортал'
<img
alt='Логотип КонцептПортал'
className='max-h-[1.6rem] min-w-[11.5rem]'
src={!darkMode ? '/logo_full.svg' : '/logo_full_dark.svg'}
/>);
/>
);
}
export default Logo;

View File

@ -13,7 +13,7 @@ import NavigationButton from './NavigationButton';
import ToggleNavigationButton from './ToggleNavigationButton';
import UserMenu from './UserMenu';
function Navigation () {
function Navigation() {
const router = useConceptNavigation();
const { noNavigationAnimation } = useConceptTheme();
@ -23,27 +23,15 @@ function Navigation () {
const navigateCreateNew = () => router.push('/library/create');
return (
<nav className={clsx(
'z-navigation',
'sticky top-0 left-0 right-0',
'clr-app',
'select-none'
)}>
<nav className={clsx('z-navigation', 'sticky top-0 left-0 right-0', 'clr-app', 'select-none')}>
<ToggleNavigationButton />
<motion.div
className={clsx(
'pl-2 pr-[0.9rem] h-[3rem]',
'flex justify-between',
'shadow-border'
)}
className={clsx('pl-2 pr-[0.9rem] h-[3rem]', 'flex justify-between', 'shadow-border')}
initial={false}
animate={!noNavigationAnimation ? 'open' : 'closed'}
variants={animateNavigation}
>
<div tabIndex={-1}
className='flex items-center mr-2 cursor-pointer'
onClick={navigateHome}
>
<div tabIndex={-1} className='flex items-center mr-2 cursor-pointer' onClick={navigateHome}>
<Logo />
</div>
<div className='flex'>
@ -68,7 +56,8 @@ function Navigation () {
<UserMenu />
</div>
</motion.div>
</nav>);
</nav>
);
}
export default Navigation;

View File

@ -3,20 +3,22 @@ import clsx from 'clsx';
import { globalIDs } from '@/utils/constants';
interface NavigationButtonProps {
text?: string
icon: React.ReactNode
title?: string
onClick?: () => void
text?: string;
icon: React.ReactNode;
title?: string;
onClick?: () => void;
}
function NavigationButton({ icon, title, onClick, text }: NavigationButtonProps) {
return (
<button type='button' tabIndex={-1}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
<button
type='button'
tabIndex={-1}
data-tooltip-id={title ? globalIDs.tooltip : undefined}
data-tooltip-content={title}
onClick={onClick}
className={clsx(
'mr-1 h-full',
'mr-1 h-full', //
'flex items-center gap-1',
'clr-btn-nav',
'small-caps whitespace-nowrap',
@ -28,7 +30,8 @@ function NavigationButton({ icon, title, onClick, text }: NavigationButtonProps)
>
{icon ? <span>{icon}</span> : null}
{text ? <span className='font-semibold'>{text}</span> : null}
</button>);
</button>
);
}
export default NavigationButton;

View File

@ -8,7 +8,9 @@ import { animateNavigationToggle } from '@/utils/animations';
function ToggleNavigationButton() {
const { noNavigationAnimation, toggleNoNavigation } = useConceptTheme();
return (
<motion.button type='button' tabIndex={-1}
<motion.button
type='button'
tabIndex={-1}
title={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
className={clsx(
'absolute top-0 right-0 z-navigation flex items-center justify-center',
@ -22,7 +24,8 @@ function ToggleNavigationButton() {
>
{!noNavigationAnimation ? <RiPushpinFill /> : null}
{noNavigationAnimation ? <RiUnpinLine /> : null}
</motion.button>);
</motion.button>
);
}
export default ToggleNavigationButton;

View File

@ -5,8 +5,8 @@ import { useConceptNavigation } from '@/context/NavigationContext';
import { useConceptTheme } from '@/context/ThemeContext';
interface UserDropdownProps {
isOpen: boolean
hideDropdown: () => void
isOpen: boolean;
hideDropdown: () => void;
}
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
@ -19,30 +19,22 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
router.push('/profile');
};
const logoutAndRedirect =
() => {
const logoutAndRedirect = () => {
hideDropdown();
logout(() => router.push('/login/'));
};
return (
<Dropdown className='w-36' stretchLeft isOpen={isOpen}>
<DropdownButton
text={user?.username}
title='Профиль пользователя'
onClick={navigateProfile}
/>
<DropdownButton text={user?.username} title='Профиль пользователя' onClick={navigateProfile} />
<DropdownButton
text={darkMode ? 'Светлая тема' : 'Темная тема'}
title='Переключение темы оформления'
onClick={toggleDarkMode}
/>
<DropdownButton
text='Выйти...'
className='font-semibold'
onClick={logoutAndRedirect}
/>
</Dropdown>);
<DropdownButton text='Выйти...' className='font-semibold' onClick={logoutAndRedirect} />
</Dropdown>
);
}
export default UserDropdown;

View File

@ -16,22 +16,17 @@ function UserMenu() {
const navigateLogin = () => router.push('/login');
return (
<div ref={menu.ref} className='h-full'>
{!user ?
{!user ? (
<NavigationButton
title='Перейти на страницу логина'
icon={<InDoorIcon size='1.5rem' className='clr-text-primary' />}
onClick={navigateLogin}
/> : null}
{user ?
<NavigationButton
icon={<FaCircleUser size='1.5rem' />}
onClick={menu.toggle}
/> : null}
<UserDropdown
isOpen={!!user && menu.isOpen}
hideDropdown={() => menu.hide()}
/>
</div>);
) : null}
{user ? <NavigationButton icon={<FaCircleUser size='1.5rem' />} onClick={menu.toggle} /> : null}
<UserDropdown isOpen={!!user && menu.isOpen} hideDropdown={() => menu.hide()} />
</div>
);
}
export default UserMenu;

View File

@ -0,0 +1,60 @@
'use client';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import { useMemo, useState } from 'react';
import { Document, Page } from 'react-pdf';
import useWindowSize from '@/hooks/useWindowSize';
import { graphLightT } from '@/utils/color';
import Overlay from '../Common/Overlay';
import PageControls from './PageControls';
const MAXIMUM_WIDTH = 1000;
const MINIMUM_WIDTH = 600;
interface PDFViewerProps {
file?: string | ArrayBuffer | Blob;
}
function PDFViewer({ file }: PDFViewerProps) {
const windowSize = useWindowSize();
const [pageCount, setPageCount] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
const pageWidth = useMemo(() => {
return Math.max(MINIMUM_WIDTH, Math.min((windowSize?.width ?? 0) - 300, MAXIMUM_WIDTH));
}, [windowSize]);
function onDocumentLoadSuccess({ numPages }: PDFDocumentProxy) {
setPageCount(numPages);
}
return (
<Document
file={file}
onLoadSuccess={onDocumentLoadSuccess}
className='px-3'
loading='Загрузка PDF файла...'
error='Не удалось загрузить файл.'
>
<Overlay position='top-6 left-1/2 -translate-x-1/2' className='flex select-none'>
<PageControls pageCount={pageCount} pageNumber={pageNumber} setPageNumber={setPageNumber} />
</Overlay>
<Page
className='pointer-events-none select-none'
renderTextLayer={false}
renderAnnotationLayer={false}
pageNumber={pageNumber}
width={pageWidth}
canvasBackground={graphLightT.canvas.background}
/>
<Overlay position='bottom-6 left-1/2 -translate-x-1/2' className='flex select-none'>
<PageControls pageCount={pageCount} pageNumber={pageNumber} setPageNumber={setPageNumber} />
</Overlay>
</Document>
);
}
export default PDFViewer;

View File

@ -0,0 +1,51 @@
import { BiChevronLeft, BiChevronRight, BiFirstPage, BiLastPage } from 'react-icons/bi';
interface PageControlsProps {
pageNumber: number;
pageCount: number;
setPageNumber: React.Dispatch<React.SetStateAction<number>>;
}
function PageControls({ pageNumber, pageCount, setPageNumber }: PageControlsProps) {
return (
<>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(1)}
disabled={pageNumber < 2}
>
<BiFirstPage size='1.5rem' />
</button>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(prev => prev - 1)}
disabled={pageNumber < 2}
>
<BiChevronLeft size='1.5rem' />
</button>
<p className='px-3 text-black'>
Страница {pageNumber} из {pageCount}
</p>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(prev => prev + 1)}
disabled={pageNumber >= pageCount}
>
<BiChevronRight size='1.5rem' />
</button>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(pageCount)}
disabled={pageNumber >= pageCount}
>
<BiLastPage size='1.5rem' />
</button>
</>
);
}
export default PageControls;

View File

@ -0,0 +1 @@
export { default } from './PDFViewer';

View File

@ -14,7 +14,7 @@ import { useConceptTheme } from '@/context/ThemeContext';
import { ccBracketMatching } from './bracketMatching';
import { RSLanguage } from './rslang';
import { getSymbolSubstitute,RSTextWrapper } from './textEditing';
import { getSymbolSubstitute, RSTextWrapper } from './textEditing';
import { rsHoverTooltip } from './tooltip';
const editorSetup: BasicSetupOptions = {
@ -46,33 +46,29 @@ const editorSetup: BasicSetupOptions = {
};
interface RSInputProps
extends Pick<ReactCodeMirrorProps,
'id' | 'height' | 'minHeight' | 'maxHeight' | 'value' |
'onFocus' | 'onBlur' | 'placeholder' | 'style' | 'className'
> {
label?: string
disabled?: boolean
noTooltip?: boolean
onChange?: (newValue: string) => void
onAnalyze?: () => void
extends Pick<
ReactCodeMirrorProps,
'id' | 'height' | 'minHeight' | 'maxHeight' | 'value' | 'onFocus' | 'onBlur' | 'placeholder' | 'style' | 'className'
> {
label?: string;
disabled?: boolean;
noTooltip?: boolean;
onChange?: (newValue: string) => void;
onAnalyze?: () => void;
}
const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
({
id, label, onChange, onAnalyze,
disabled, noTooltip,
className, style,
...restProps
}, ref) => {
({ id, label, onChange, onAnalyze, disabled, noTooltip, className, style, ...restProps }, ref) => {
const { darkMode, colors } = useConceptTheme();
const { schema } = useRSForm();
const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
const cursor = useMemo(() => !disabled ? 'cursor-text': 'cursor-default', [disabled]);
const cursor = useMemo(() => (!disabled ? 'cursor-text' : 'cursor-default'), [disabled]);
const customTheme: Extension = useMemo(
() => createTheme({
() =>
createTheme({
theme: darkMode ? 'dark' : 'light',
settings: {
fontFamily: 'inherit',
@ -86,19 +82,23 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
{ tag: tags.propertyName, color: colors.fgTeal }, // Radical
{ tag: tags.keyword, color: colors.fgBlue }, // keywords
{ tag: tags.literal, color: colors.fgBlue }, // literals
{ tag: tags.controlKeyword, fontWeight: '500'}, // R | I | D
{ tag: tags.controlKeyword, fontWeight: '500' }, // R | I | D
{ tag: tags.unit, fontSize: '0.75rem' }, // indices
{ tag: tags.brace, color:colors.fgPurple, fontWeight: '700' }, // braces (curly brackets)
{ tag: tags.brace, color: colors.fgPurple, fontWeight: '700' } // braces (curly brackets)
]
}), [disabled, colors, darkMode]);
}),
[disabled, colors, darkMode]
);
const editorExtensions = useMemo(
() => [
EditorView.lineWrapping,
RSLanguage,
ccBracketMatching(darkMode),
... noTooltip ? [] : [rsHoverTooltip(schema?.items || [])],
], [darkMode, schema?.items, noTooltip]);
...(noTooltip ? [] : [rsHoverTooltip(schema?.items || [])])
],
[darkMode, schema?.items, noTooltip]
);
const handleInput = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
@ -123,19 +123,15 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
event.preventDefault();
event.stopPropagation();
}
}, [thisRef, onAnalyze]);
},
[thisRef, onAnalyze]
);
return (
<div
className={clsx(
'flex flex-col gap-2',
className,
cursor
)}
style={style}
>
<Label text={label} htmlFor={id}/>
<CodeMirror id={id}
<div className={clsx('flex flex-col gap-2', className, cursor)} style={style}>
<Label text={label} htmlFor={id} />
<CodeMirror
id={id}
ref={thisRef}
basicSetup={editorSetup}
theme={customTheme}
@ -146,7 +142,9 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
onKeyDown={handleInput}
{...restProps}
/>
</div>);
});
</div>
);
}
);
export default RSInput;

View File

@ -3,8 +3,8 @@ import { Decoration, EditorView } from '@codemirror/view';
import { bracketsDarkT, bracketsLightT } from '@/utils/color';
const matchingMark = Decoration.mark({class: 'cc-matchingBracket'});
const nonMatchingMark = Decoration.mark({class: 'cc-nonmatchingBracket'});
const matchingMark = Decoration.mark({ class: 'cc-matchingBracket' });
const nonMatchingMark = Decoration.mark({ class: 'cc-nonmatchingBracket' });
function bracketRender(match: MatchResult) {
const decorations = [];
@ -24,7 +24,7 @@ export function ccBracketMatching(darkMode: boolean) {
return [
bracketMatching({
renderMatch: bracketRender,
brackets:'{}[]()'
brackets: '{}[]()'
}),
darkMode ? darkTheme : lightTheme
];

View File

@ -1,23 +1,23 @@
import {styleTags, tags} from '@lezer/highlight';
import { styleTags, tags } from '@lezer/highlight';
export const highlighting = styleTags({
Index: tags.unit,
ComplexIndex: tags.unit,
Literal: tags.literal,
'Index': tags.unit,
'ComplexIndex': tags.unit,
'Literal': tags.literal,
Radical: tags.propertyName,
Function: tags.name,
Predicate: tags.name,
Global: tags.name,
Local: tags.variableName,
'Radical': tags.propertyName,
'Function': tags.name,
'Predicate': tags.name,
'Global': tags.name,
'Local': tags.variableName,
TextFunction: tags.keyword,
Filter: tags.keyword,
PrefixR: tags.controlKeyword,
PrefixI: tags.controlKeyword,
PrefixD: tags.controlKeyword,
"{": tags.brace,
"}": tags.brace,
"|": tags.brace,
";": tags.brace,
'TextFunction': tags.keyword,
'Filter': tags.keyword,
'PrefixR': tags.controlKeyword,
'PrefixI': tags.controlKeyword,
'PrefixD': tags.controlKeyword,
'{': tags.brace,
'}': tags.brace,
'|': tags.brace,
';': tags.brace
});

View File

@ -1,11 +1,9 @@
import {LRLanguage} from '@codemirror/language';
import { LRLanguage } from '@codemirror/language';
import { parser } from './parser';
import { Function, Global, Predicate } from './parser.terms';
export const GlobalTokens: number[] = [
Global, Function, Predicate
]
export const GlobalTokens: number[] = [Global, Function, Predicate];
export const RSLanguage = LRLanguage.define({
parser: parser,

View File

@ -6,6 +6,7 @@ import { TokenID } from '@/models/rslang';
import { CodeMirrorWrapper } from '@/utils/codemirror';
export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): string | undefined {
// prettier-ignore
if (shiftPressed) {
switch (keyCode) {
case 'Backquote': return '∃';
@ -59,7 +60,7 @@ export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): str
/**
* Wrapper class for RSLang editor.
*/
*/
export class RSTextWrapper extends CodeMirrorWrapper {
constructor(object: Required<ReactCodeMirrorRef>) {
super(object);
@ -67,7 +68,7 @@ export class RSTextWrapper extends CodeMirrorWrapper {
insertToken(tokenID: TokenID): boolean {
const selection = this.getSelection();
const hasSelection = selection.from !== selection.to
const hasSelection = selection.from !== selection.to;
switch (tokenID) {
case TokenID.NT_DECLARATIVE_EXPR: {
if (hasSelection) {
@ -77,7 +78,7 @@ export class RSTextWrapper extends CodeMirrorWrapper {
}
this.ref.view.dispatch({
selection: {
anchor: selection.from + 2,
anchor: selection.from + 2
}
});
return true;
@ -98,19 +99,33 @@ export class RSTextWrapper extends CodeMirrorWrapper {
}
return true;
}
case TokenID.BIGPR: this.envelopeWith('Pr1(', ')'); return true;
case TokenID.SMALLPR: this.envelopeWith('pr1(', ')'); return true;
case TokenID.FILTER: this.envelopeWith('Fi1[α](', ')'); return true;
case TokenID.REDUCE: this.envelopeWith('red(', ')'); return true;
case TokenID.CARD: this.envelopeWith('card(', ')'); return true;
case TokenID.BOOL: this.envelopeWith('bool(', ')'); return true;
case TokenID.DEBOOL: this.envelopeWith('debool(', ')'); return true;
case TokenID.BIGPR:
this.envelopeWith('Pr1(', ')');
return true;
case TokenID.SMALLPR:
this.envelopeWith('pr1(', ')');
return true;
case TokenID.FILTER:
this.envelopeWith('Fi1[α](', ')');
return true;
case TokenID.REDUCE:
this.envelopeWith('red(', ')');
return true;
case TokenID.CARD:
this.envelopeWith('card(', ')');
return true;
case TokenID.BOOL:
this.envelopeWith('bool(', ')');
return true;
case TokenID.DEBOOL:
this.envelopeWith('debool(', ')');
return true;
case TokenID.PUNCTUATION_PL: {
this.envelopeWith('(', ')');
this.ref.view.dispatch({
selection: {
anchor: hasSelection ? selection.to: selection.from + 1,
anchor: hasSelection ? selection.to : selection.from + 1
}
});
return true;
@ -120,7 +135,7 @@ export class RSTextWrapper extends CodeMirrorWrapper {
if (hasSelection) {
this.ref.view.dispatch({
selection: {
anchor: hasSelection ? selection.to: selection.from + 1,
anchor: hasSelection ? selection.to : selection.from + 1
}
});
}
@ -136,37 +151,90 @@ export class RSTextWrapper extends CodeMirrorWrapper {
return true;
}
case TokenID.DECART: this.replaceWith('×'); return true;
case TokenID.QUANTOR_UNIVERSAL: this.replaceWith('∀'); return true;
case TokenID.QUANTOR_EXISTS: this.replaceWith('∃'); return true;
case TokenID.SET_IN: this.replaceWith('∈'); return true;
case TokenID.SET_NOT_IN: this.replaceWith('∉'); return true;
case TokenID.LOGIC_OR: this.replaceWith(''); return true;
case TokenID.LOGIC_AND: this.replaceWith('&'); return true;
case TokenID.SUBSET_OR_EQ: this.replaceWith('⊆'); return true;
case TokenID.LOGIC_IMPLICATION: this.replaceWith('⇒'); return true;
case TokenID.SET_INTERSECTION: this.replaceWith('∩'); return true;
case TokenID.SET_UNION: this.replaceWith(''); return true;
case TokenID.SET_MINUS: this.replaceWith('\\'); return true;
case TokenID.SET_SYMMETRIC_MINUS: this.replaceWith('∆'); return true;
case TokenID.LIT_EMPTYSET: this.replaceWith('∅'); return true;
case TokenID.LIT_WHOLE_NUMBERS: this.replaceWith('Z'); return true;
case TokenID.SUBSET: this.replaceWith('⊂'); return true;
case TokenID.NOT_SUBSET: this.replaceWith('⊄'); return true;
case TokenID.EQUAL: this.replaceWith('='); return true;
case TokenID.NOTEQUAL: this.replaceWith('≠'); return true;
case TokenID.LOGIC_NOT: this.replaceWith('¬'); return true;
case TokenID.LOGIC_EQUIVALENT: this.replaceWith('⇔'); return true;
case TokenID.GREATER_OR_EQ: this.replaceWith('≥'); return true;
case TokenID.LESSER_OR_EQ: this.replaceWith('≤'); return true;
case TokenID.PUNCTUATION_ASSIGN: this.replaceWith(':='); return true;
case TokenID.PUNCTUATION_ITERATE: this.replaceWith(':∈'); return true;
case TokenID.MULTIPLY: this.replaceWith('*'); return true;
case TokenID.DECART:
this.replaceWith('×');
return true;
case TokenID.QUANTOR_UNIVERSAL:
this.replaceWith('∀');
return true;
case TokenID.QUANTOR_EXISTS:
this.replaceWith('∃');
return true;
case TokenID.SET_IN:
this.replaceWith('∈');
return true;
case TokenID.SET_NOT_IN:
this.replaceWith('∉');
return true;
case TokenID.LOGIC_OR:
this.replaceWith('');
return true;
case TokenID.LOGIC_AND:
this.replaceWith('&');
return true;
case TokenID.SUBSET_OR_EQ:
this.replaceWith('⊆');
return true;
case TokenID.LOGIC_IMPLICATION:
this.replaceWith('⇒');
return true;
case TokenID.SET_INTERSECTION:
this.replaceWith('∩');
return true;
case TokenID.SET_UNION:
this.replaceWith('');
return true;
case TokenID.SET_MINUS:
this.replaceWith('\\');
return true;
case TokenID.SET_SYMMETRIC_MINUS:
this.replaceWith('∆');
return true;
case TokenID.LIT_EMPTYSET:
this.replaceWith('∅');
return true;
case TokenID.LIT_WHOLE_NUMBERS:
this.replaceWith('Z');
return true;
case TokenID.SUBSET:
this.replaceWith('⊂');
return true;
case TokenID.NOT_SUBSET:
this.replaceWith('⊄');
return true;
case TokenID.EQUAL:
this.replaceWith('=');
return true;
case TokenID.NOTEQUAL:
this.replaceWith('≠');
return true;
case TokenID.LOGIC_NOT:
this.replaceWith('¬');
return true;
case TokenID.LOGIC_EQUIVALENT:
this.replaceWith('⇔');
return true;
case TokenID.GREATER_OR_EQ:
this.replaceWith('≥');
return true;
case TokenID.LESSER_OR_EQ:
this.replaceWith('≤');
return true;
case TokenID.PUNCTUATION_ASSIGN:
this.replaceWith(':=');
return true;
case TokenID.PUNCTUATION_ITERATE:
this.replaceWith(':∈');
return true;
case TokenID.MULTIPLY:
this.replaceWith('*');
return true;
}
return false;
}
processAltKey(keyCode: string, shiftPressed: boolean): boolean {
// prettier-ignore
if (shiftPressed) {
switch (keyCode) {
case 'KeyE': return this.insertToken(TokenID.DECART);

View File

@ -1,4 +1,4 @@
import { syntaxTree } from "@codemirror/language"
import { syntaxTree } from '@codemirror/language';
import { Extension } from '@codemirror/state';
import { hoverTooltip } from '@codemirror/view';
import { EditorState } from '@uiw/react-codemirror';
@ -15,15 +15,14 @@ function findAliasAt(pos: number, state: EditorState) {
let alias = '';
let start = 0;
let end = 0;
nodes.forEach(
node => {
nodes.forEach(node => {
if (node.to <= lineEnd && node.from >= lineStart) {
alias = text.slice(node.from - lineStart, node.to - lineStart);
start = node.from;
end = node.to;
}
});
return {alias, start, end};
return { alias, start, end };
}
const globalsHoverTooltip = (items: IConstituenta[]) => {
@ -38,9 +37,9 @@ const globalsHoverTooltip = (items: IConstituenta[]) => {
end: end,
above: false,
create: () => domTooltipConstituenta(cst)
}
};
});
}
};
export function rsHoverTooltip(items: IConstituenta[]): Extension {
return [globalsHoverTooltip(items)];

View File

@ -50,26 +50,19 @@ const editorSetup: BasicSetupOptions = {
};
interface RefsInputInputProps
extends Pick<ReactCodeMirrorProps,
'id'| 'height' | 'value' | 'className' | 'onFocus' | 'onBlur' | 'placeholder'
> {
label?: string
onChange?: (newValue: string) => void
items?: IConstituenta[]
disabled?: boolean
extends Pick<ReactCodeMirrorProps, 'id' | 'height' | 'value' | 'className' | 'onFocus' | 'onBlur' | 'placeholder'> {
label?: string;
onChange?: (newValue: string) => void;
items?: IConstituenta[];
disabled?: boolean;
initialValue?: string
value?: string
resolved?: string
initialValue?: string;
value?: string;
resolved?: string;
}
const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
({
id, label, disabled, items,
initialValue, value, resolved,
onFocus, onBlur, onChange,
...restProps
}, ref) => {
({ id, label, disabled, items, initialValue, value, resolved, onFocus, onBlur, onChange, ...restProps }, ref) => {
const { darkMode, colors } = useConceptTheme();
const { schema } = useRSForm();
@ -85,9 +78,10 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
const cursor = useMemo(() => !disabled ? 'cursor-text': 'cursor-default', [disabled]);
const cursor = useMemo(() => (!disabled ? 'cursor-text' : 'cursor-default'), [disabled]);
const customTheme: Extension = useMemo(
() => createTheme({
() =>
createTheme({
theme: darkMode ? 'dark' : 'light',
settings: {
fontFamily: 'inherit',
@ -98,16 +92,16 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
styles: [
{ tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // EntityReference
{ tag: tags.literal, color: colors.fgTeal, cursor: 'default' }, // SyntacticReference
{ tag: tags.comment, color: colors.fgRed }, // Error
{ tag: tags.comment, color: colors.fgRed } // Error
]
}), [disabled, colors, darkMode]);
}),
[disabled, colors, darkMode]
);
const editorExtensions = useMemo(
() => [
EditorView.lineWrapping,
NaturalLanguage,
refsHoverTooltip(schema?.items || [], colors)
], [schema?.items, colors]);
() => [EditorView.lineWrapping, NaturalLanguage, refsHoverTooltip(schema?.items || [], colors)],
[schema?.items, colors]
);
function handleChange(newValue: string) {
if (onChange) onChange(newValue);
@ -143,13 +137,17 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
}
const selection = wrap.getSelection();
const mainNodes = wrap.getAllNodes([RefEntity]).filter(node => node.from >= selection.to || node.to <= selection.from);
const mainNodes = wrap
.getAllNodes([RefEntity])
.filter(node => node.from >= selection.to || node.to <= selection.from);
setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to)));
setBasePosition(mainNodes.filter(node => node.to <= selection.from).length);
setShowEditor(true);
}
}, [thisRef]);
},
[thisRef]
);
const handleInputReference = useCallback(
(referenceText: string) => {
@ -159,11 +157,14 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
thisRef.current.view.focus();
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
wrap.replaceWith(referenceText);
}, [thisRef]);
},
[thisRef]
);
return (<>
return (
<>
<AnimatePresence>
{showEditor ?
{showEditor ? (
<DlgEditReference
hideWindow={() => setShowEditor(false)}
items={items ?? []}
@ -175,32 +176,32 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
mainRefs: mainRefs
}}
onSave={handleInputReference}
/> : null}
/>
) : null}
</AnimatePresence>
<div className={clsx(
'flex flex-col gap-2',
cursor
)}>
<div className={clsx('flex flex-col gap-2', cursor)}>
<Label text={label} htmlFor={id} />
<CodeMirror id={id} ref={thisRef}
<CodeMirror
id={id}
ref={thisRef}
basicSetup={editorSetup}
theme={customTheme}
extensions={editorExtensions}
value={isFocused ? value : (value !== initialValue || showEditor ? value : resolved)}
value={isFocused ? value : value !== initialValue || showEditor ? value : resolved}
indentWithTab={false}
onChange={handleChange}
editable={!disabled}
onKeyDown={handleInput}
onFocus={handleFocusIn}
onBlur={handleFocusOut}
// spellCheck= // TODO: figure out while automatic spellcheck doesnt work or implement with extension
// spellCheck= // TODO: figure out while automatic spellcheck doesn't work or implement with extension
{...restProps}
/>
</div>
</>);
});
</>
);
}
);
export default RefsInput;

View File

@ -1,4 +1,4 @@
import {styleTags, tags} from '@lezer/highlight';
import { styleTags, tags } from '@lezer/highlight';
export const highlighting = styleTags({
RefEntity: tags.name,

View File

@ -1,11 +1,9 @@
import {LRLanguage} from '@codemirror/language';
import { LRLanguage } from '@codemirror/language';
import { parser } from './parser';
import { RefEntity, RefSyntactic } from './parser.terms';
export const ReferenceTokens: number[] = [
RefSyntactic, RefEntity
]
export const ReferenceTokens: number[] = [RefSyntactic, RefEntity];
export const NaturalLanguage = LRLanguage.define({
parser: parser,

View File

@ -13,12 +13,11 @@ const testData = [
['@{-1| черный }', '[Text[RefSyntactic[Offset][Nominal]]]'],
['@{-100| черный слон }', '[Text[RefSyntactic[Offset][Nominal]]]'],
['@{X1|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]'],
['@{X12|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]'],
['@{X12|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]']
];
describe('Testing NaturalParser', () => {
it.each(testData)('Parse %p',
(input: string, expectedTree: string) => {
it.each(testData)('Parse %p', (input: string, expectedTree: string) => {
// NOTE: use strict parser to determine exact error position
// const tree = parser.configure({strict: true}).parse(input);
const tree = parser.parse(input);

View File

@ -1,18 +1,22 @@
import { syntaxTree } from '@codemirror/language'
import { syntaxTree } from '@codemirror/language';
import { Extension } from '@codemirror/state';
import { hoverTooltip } from '@codemirror/view';
import { parseEntityReference, parseSyntacticReference } from '@/models/languageAPI';
import { IConstituenta } from '@/models/rsform';
import { domTooltipEntityReference, domTooltipSyntacticReference, findContainedNodes, findEnvelopingNodes } from '@/utils/codemirror';
import {
domTooltipEntityReference,
domTooltipSyntacticReference,
findContainedNodes,
findEnvelopingNodes
} from '@/utils/codemirror';
import { IColorTheme } from '@/utils/color';
import { ReferenceTokens } from './parse';
import { RefEntity, RefSyntactic } from './parse/parser.terms';
export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme) => {
return hoverTooltip(
(view, pos) => {
return hoverTooltip((view, pos) => {
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), ReferenceTokens);
if (nodes.length !== 1) {
return null;
@ -28,7 +32,7 @@ export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme)
end: end,
above: false,
create: () => domTooltipEntityReference(ref, cst, colors)
}
};
} else if (nodes[0].type.id === RefSyntactic) {
const ref = parseSyntacticReference(text);
let masterText: string | undefined = undefined;
@ -50,12 +54,12 @@ export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme)
end: end,
above: false,
create: () => domTooltipSyntacticReference(ref, masterText)
}
};
} else {
return null;
}
});
}
};
export function refsHoverTooltip(items: IConstituenta[], colors: IColorTheme): Extension {
return [globalsHoverTooltip(items, colors)];

View File

@ -5,7 +5,7 @@ import { useAuth } from '@/context/AuthContext';
import TextURL from './Common/TextURL';
interface RequireAuthProps {
children: React.ReactNode
children: React.ReactNode;
}
function RequireAuth({ children }: RequireAuthProps) {
@ -17,10 +17,11 @@ function RequireAuth({ children }: RequireAuthProps) {
return (
<div className='flex flex-col items-center gap-1 mt-2'>
<p className='mb-2'>Пожалуйста войдите в систему</p>
<TextURL text='Войти в Портал' href='/login'/>
<TextURL text='Зарегистрироваться' href='/signup'/>
<TextURL text='Начальная страница' href='/'/>
</div>);
<TextURL text='Войти в Портал' href='/login' />
<TextURL text='Зарегистрироваться' href='/signup' />
<TextURL text='Начальная страница' href='/' />
</div>
);
}
}

View File

@ -4,14 +4,14 @@ import ConceptTooltip from '@/components/Common/ConceptTooltip';
import ConstituentaTooltip from '@/components/Help/ConstituentaTooltip';
import { IConstituenta } from '@/models/rsform';
import { isMockCst } from '@/models/rsformAPI';
import { colorFgCstStatus,IColorTheme } from '@/utils/color';
import { colorFgCstStatus, IColorTheme } from '@/utils/color';
import { describeExpressionStatus } from '@/utils/labels';
interface ConstituentaBadgeProps {
prefixID?: string
shortTooltip?: boolean
value: IConstituenta
theme: IColorTheme
prefixID?: string;
shortTooltip?: boolean;
value: IConstituenta;
theme: IColorTheme;
}
function ConstituentaBadge({ value, prefixID, shortTooltip, theme }: ConstituentaBadgeProps) {
@ -31,19 +31,16 @@ function ConstituentaBadge({ value, prefixID, shortTooltip, theme }: Constituent
}}
>
{value.alias}
{!shortTooltip ?
<ConstituentaTooltip
anchor={`#${prefixID}${value.alias}`}
data={value}
/> : null}
{shortTooltip ?
<ConceptTooltip
anchorSelect={`#${prefixID}${value.alias}`}
place='right'
>
<p><b>Статус</b>: {describeExpressionStatus(value.status)}</p>
</ConceptTooltip> : null}
</div>);
{!shortTooltip ? <ConstituentaTooltip anchor={`#${prefixID}${value.alias}`} data={value} /> : null}
{shortTooltip ? (
<ConceptTooltip anchorSelect={`#${prefixID}${value.alias}`} place='right'>
<p>
<b>Статус</b>: {describeExpressionStatus(value.status)}
</p>
</ConceptTooltip>
) : null}
</div>
);
}
export default ConstituentaBadge;

View File

@ -12,35 +12,35 @@ import { describeConstituenta } from '@/utils/labels';
import ConstituentaBadge from './ConstituentaBadge';
interface ConstituentaPickerProps {
prefixID?: string
data?: IConstituenta[]
rows?: number
prefixID?: string;
data?: IConstituenta[];
rows?: number;
onBeginFilter?: (cst: IConstituenta) => boolean
describeFunc?: (cst: IConstituenta) => string
matchFunc?: (cst: IConstituenta, filter: string) => boolean
onBeginFilter?: (cst: IConstituenta) => boolean;
describeFunc?: (cst: IConstituenta) => string;
matchFunc?: (cst: IConstituenta, filter: string) => boolean;
value?: IConstituenta
onSelectValue: (newValue: IConstituenta) => void
value?: IConstituenta;
onSelectValue: (newValue: IConstituenta) => void;
}
const columnHelper = createColumnHelper<IConstituenta>();
function ConstituentaPicker({
data, value,
data,
value,
rows = 4,
prefixID = prefixes.cst_list,
describeFunc = describeConstituenta,
matchFunc = (cst, filter) => matchConstituenta(cst, filter, CstMatchMode.ALL),
onBeginFilter,
onSelectValue
} : ConstituentaPickerProps) {
}: ConstituentaPickerProps) {
const { colors } = useConceptTheme();
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);
const [filterText, setFilterText] = useState('');
useEffect(
() => {
useEffect(() => {
if (!data) {
setFilteredData([]);
} else {
@ -51,7 +51,6 @@ function ConstituentaPicker({
setFilteredData(newData);
}
}
}, [data, filterText, matchFunc, onBeginFilter]);
const columns = useMemo(
@ -61,33 +60,34 @@ function ConstituentaPicker({
size: 65,
minSize: 65,
maxSize: 65,
cell: props =>
<ConstituentaBadge
theme={colors}
value={props.row.original}
prefixID={prefixID}
/>
cell: props => <ConstituentaBadge theme={colors} value={props.row.original} prefixID={prefixID} />
}),
columnHelper.accessor(cst => describeFunc(cst), {
id: 'description'
})
], [colors, prefixID, describeFunc]);
],
[colors, prefixID, describeFunc]
);
const size = useMemo(() => (`calc(2px + (2px + 1.8rem)*${rows})`), [rows]);
const size = useMemo(() => `calc(2px + (2px + 1.8rem)*${rows})`, [rows]);
const conditionalRowStyles = useMemo(
(): IConditionalStyle<IConstituenta>[] => [{
(): IConditionalStyle<IConstituenta>[] => [
{
when: (cst: IConstituenta) => cst.id === value?.id,
style: { backgroundColor: colors.bgSelected },
}], [value, colors]);
style: { backgroundColor: colors.bgSelected }
}
],
[value, colors]
);
return (
<div>
<ConceptSearch
value={filterText}
onChange={newValue => setFilterText(newValue)}
/>
<DataTable dense noHeader noFooter
<ConceptSearch value={filterText} onChange={newValue => setFilterText(newValue)} />
<DataTable
dense
noHeader
noFooter
className='overflow-y-auto text-sm border select-none'
style={{ maxHeight: size, minHeight: size }}
data={filteredData}
@ -101,7 +101,8 @@ function ConstituentaPicker({
}
onRowClicked={onSelectValue}
/>
</div>);
</div>
);
}
export default ConstituentaPicker;

View File

@ -6,8 +6,8 @@ import { colorFgGrammeme } from '@/utils/color';
import { labelGrammeme } from '@/utils/labels';
interface GrammemeBadgeProps {
key?: string
grammeme: GramData
key?: string;
grammeme: GramData;
}
function GrammemeBadge({ key, grammeme }: GrammemeBadgeProps) {
@ -28,7 +28,8 @@ function GrammemeBadge({ key, grammeme }: GrammemeBadgeProps) {
}}
>
{labelGrammeme(grammeme)}
</div>);
</div>
);
}
export default GrammemeBadge;

View File

@ -1,9 +1,8 @@
import { IConstituenta } from '@/models/rsform';
import { labelCstTypification } from '@/utils/labels';
interface InfoConstituentaProps
extends React.HTMLAttributes<HTMLDivElement> {
data: IConstituenta
interface InfoConstituentaProps extends React.HTMLAttributes<HTMLDivElement> {
data: IConstituenta;
}
function InfoConstituenta({ data, ...restProps }: InfoConstituentaProps) {
@ -18,22 +17,26 @@ function InfoConstituenta({ data, ...restProps }: InfoConstituentaProps) {
<b>Термин: </b>
{data.term_resolved || data.term_raw}
</p>
{data.definition_formal ?
{data.definition_formal ? (
<p>
<b>Выражение: </b>
{data.definition_formal}
</p> : null}
{data.definition_resolved ?
</p>
) : null}
{data.definition_resolved ? (
<p>
<b>Определение: </b>
{data.definition_resolved}
</p> : null}
{data.convention ?
</p>
) : null}
{data.convention ? (
<p>
<b>Конвенция: </b>
{data.convention}
</p> : null}
</div>);
</p>
) : null}
</div>
);
}
export default InfoConstituenta;

View File

@ -7,7 +7,7 @@ import { prefixes } from '@/utils/constants';
import { describeCstClass, labelCstClass } from '@/utils/labels';
interface InfoCstClassProps {
header?: string
header?: string;
}
function InfoCstClass({ header }: InfoCstClassProps) {
@ -16,8 +16,7 @@ function InfoCstClass({ header }: InfoCstClassProps) {
return (
<div className='flex flex-col gap-1 mb-2'>
{header ? <h1>{header}</h1> : null}
{Object.values(CstClass).map(
(cclass, index) => {
{Object.values(CstClass).map((cstClass, index) => {
return (
<p key={`${prefixes.cst_status_list}${index}`}>
<span
@ -28,17 +27,17 @@ function InfoCstClass({ header }: InfoCstClassProps) {
'border',
'text-center text-sm small-caps font-semibold'
)}
style={{backgroundColor: colorBgCstClass(cclass, colors)}}
style={{ backgroundColor: colorBgCstClass(cstClass, colors) }}
>
{labelCstClass(cclass)}
{labelCstClass(cstClass)}
</span>
<span> - </span>
<span>
{describeCstClass(cclass)}
</span>
</p>);
<span>{describeCstClass(cstClass)}</span>
</p>
);
})}
</div>);
</div>
);
}
export default InfoCstClass;

View File

@ -7,7 +7,7 @@ import { prefixes } from '@/utils/constants';
import { describeExpressionStatus, labelExpressionStatus } from '@/utils/labels';
interface InfoCstStatusProps {
title?: string
title?: string;
}
function InfoCstStatus({ title }: InfoCstStatusProps) {
@ -18,8 +18,7 @@ function InfoCstStatus({ title }: InfoCstStatusProps) {
{title ? <h1>{title}</h1> : null}
{Object.values(ExpressionStatus)
.filter(status => status !== ExpressionStatus.UNDEFINED)
.map(
(status, index) =>
.map((status, index) => (
<p key={`${prefixes.cst_status_list}${index}`}>
<span
className={clsx(
@ -29,17 +28,16 @@ function InfoCstStatus({ title }: InfoCstStatusProps) {
'border',
'text-center text-sm small-caps font-semibold'
)}
style={{backgroundColor: colorBgCstStatus(status, colors)}}
style={{ backgroundColor: colorBgCstStatus(status, colors) }}
>
{labelExpressionStatus(status)}
</span>
<span> - </span>
<span>
{describeExpressionStatus(status)}
</span>
<span>{describeExpressionStatus(status)}</span>
</p>
)}
</div>);
))}
</div>
);
}
export default InfoCstStatus;

View File

@ -4,7 +4,7 @@ import { useUsers } from '@/context/UsersContext';
import { ILibraryItemEx } from '@/models/library';
interface InfoLibraryItemProps {
item?: ILibraryItemEx
item?: ILibraryItemEx;
}
function InfoLibraryItem({ item }: InfoLibraryItemProps) {
@ -21,7 +21,7 @@ function InfoLibraryItem({ item }: InfoLibraryItemProps) {
<div className='flex'>
<label className='font-semibold'>Отслеживают:</label>
<span id='subscriber-count' className='ml-2'>
{ item?.subscribers.length ?? 0 }
{item?.subscribers.length ?? 0}
</span>
</div>
<div className='flex'>
@ -32,7 +32,8 @@ function InfoLibraryItem({ item }: InfoLibraryItemProps) {
<label className='font-semibold'>Дата создания:</label>
<span className='ml-8'>{item && new Date(item?.time_create).toLocaleString(intl.locale)}</span>
</div>
</div>);
</div>
);
}
export default InfoLibraryItem;

View File

@ -3,30 +3,23 @@ import { useEffect, useState } from 'react';
import SelectMulti, { SelectMultiProps } from '@/components/Common/SelectMulti';
import { Grammeme } from '@/models/language';
import { getCompatibleGrams } from '@/models/languageAPI';
import { compareGrammemeOptions,IGrammemeOption, SelectorGrammemes } from '@/utils/selectors';
import { compareGrammemeOptions, IGrammemeOption, SelectorGrammemes } from '@/utils/selectors';
interface SelectGrammemeProps extends
Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'> {
value: IGrammemeOption[]
setValue: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>
className?: string
placeholder?: string
interface SelectGrammemeProps extends Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'> {
value: IGrammemeOption[];
setValue: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>;
className?: string;
placeholder?: string;
}
function SelectGrammeme({
value, setValue,
...restProps
}: SelectGrammemeProps) {
function SelectGrammeme({ value, setValue, ...restProps }: SelectGrammemeProps) {
const [options, setOptions] = useState<IGrammemeOption[]>([]);
useEffect(
() => {
useEffect(() => {
const compatible = getCompatibleGrams(
value
.filter(data => Object.values(Grammeme).includes(data.value as Grammeme))
.map(data => data.value as Grammeme)
value.filter(data => Object.values(Grammeme).includes(data.value as Grammeme)).map(data => data.value as Grammeme)
);
setOptions(SelectorGrammemes.filter(({value}) => compatible.includes(value as Grammeme)));
setOptions(SelectorGrammemes.filter(({ value }) => compatible.includes(value as Grammeme)));
}, [value]);
return (
@ -35,7 +28,8 @@ function SelectGrammeme({
value={value}
onChange={newValue => setValue([...newValue].sort(compareGrammemeOptions))}
{...restProps}
/>);
/>
);
}
export default SelectGrammeme;

View File

@ -1,26 +1,21 @@
import Overlay from '@/components/Common/Overlay';
interface SelectedCounterProps {
total: number
selected: number
position?: string
hideZero?: boolean
total: number;
selected: number;
position?: string;
hideZero?: boolean;
}
function SelectedCounter({
total, selected, hideZero,
position = 'top-0 left-0',
} : SelectedCounterProps) {
function SelectedCounter({ total, selected, hideZero, position = 'top-0 left-0' }: SelectedCounterProps) {
if (selected === 0 && hideZero) {
return null;
}
return (
<Overlay
position={`px-2 ${position}`}
className='select-none whitespace-nowrap small-caps clr-app'
>
<Overlay position={`px-2 ${position}`} className='select-none whitespace-nowrap small-caps clr-app'>
Выбор {selected} из {total}
</Overlay>);
</Overlay>
);
}
export default SelectedCounter;

View File

@ -3,21 +3,18 @@ import { IWordForm } from '@/models/language';
import GrammemeBadge from './GrammemeBadge';
interface WordFormBadgeProps {
keyPrefix?: string
form: IWordForm
keyPrefix?: string;
form: IWordForm;
}
function WordFormBadge({ keyPrefix, form }: WordFormBadgeProps) {
return (
<div className='flex flex-wrap justify-start gap-1 select-none'>
{form.grams.map(
(gram) =>
<GrammemeBadge
key={`${keyPrefix}-${gram}`}
grammeme={gram}
/>
)}
</div>);
{form.grams.map(gram => (
<GrammemeBadge key={`${keyPrefix}-${gram}`} grammeme={gram} />
))}
</div>
);
}
export default WordFormBadge;

View File

@ -2,39 +2,40 @@
import { HTMLMotionProps } from 'framer-motion';
export namespace CProps {
export type Control = {
title?: string;
disabled?: boolean;
noBorder?: boolean;
noOutline?: boolean;
};
export type Control = {
title?: string
disabled?: boolean
noBorder?: boolean
noOutline?: boolean
}
export type Styling = {
style?: React.CSSProperties;
className?: string;
};
export type Styling = {
style?: React.CSSProperties
className?: string
}
export type Editor = Control & {
label?: string;
};
export type Editor = Control & {
label?: string
}
export type Colors = {
colors?: string;
};
export type Colors = {
colors?: string
}
export type Div = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
export type Button = Omit<
export type Div = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
export type Button = Omit<
React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>,
'children' | 'type'
>;
export type Label = Omit<
>;
export type Label = Omit<
React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>,
'children'
>;
export type TextArea = React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>;
export type Input = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
export type AnimatedButton = Omit<HTMLMotionProps<'button'>, 'type'>;
>;
export type TextArea = React.DetailedHTMLProps<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
HTMLTextAreaElement
>;
export type Input = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
export type AnimatedButton = Omit<HTMLMotionProps<'button'>, 'type'>;
}

View File

@ -5,32 +5,25 @@ import { createContext, useContext, useState } from 'react';
import { UserAccessMode } from '@/models/miscellaneous';
interface IAccessModeContext {
mode: UserAccessMode
setMode: React.Dispatch<React.SetStateAction<UserAccessMode>>
mode: UserAccessMode;
setMode: React.Dispatch<React.SetStateAction<UserAccessMode>>;
}
const AccessContext = createContext<IAccessModeContext | null>(null);
export const useAccessMode = () => {
const context = useContext(AccessContext);
if (!context) {
throw new Error(
'useAccessMode has to be used within <AccessModeState.Provider>'
);
throw new Error('useAccessMode has to be used within <AccessModeState.Provider>');
}
return context;
}
};
interface AccessModeStateProps {
children: React.ReactNode
children: React.ReactNode;
}
export const AccessModeState = ({ children }: AccessModeStateProps) => {
const [mode, setMode] = useState<UserAccessMode>(UserAccessMode.READER);
return (
<AccessContext.Provider
value={{ mode, setMode }}
>
{children}
</AccessContext.Provider>);
return <AccessContext.Provider value={{ mode, setMode }}>{children}</AccessContext.Provider>;
};

View File

@ -10,34 +10,32 @@ import { IUserSignupData } from '@/models/library';
import { IUserProfile } from '@/models/library';
import { IUserInfo } from '@/models/library';
import { IUserUpdatePassword } from '@/models/library';
import { type DataCallback, getAuth, patchPassword,postLogin, postLogout, postSignup } from '@/utils/backendAPI';
import { type DataCallback, getAuth, patchPassword, postLogin, postLogout, postSignup } from '@/utils/backendAPI';
import { useUsers } from './UsersContext';
interface IAuthContext {
user: ICurrentUser | undefined
login: (data: IUserLoginData, callback?: DataCallback) => void
logout: (callback?: DataCallback) => void
signup: (data: IUserSignupData, callback?: DataCallback<IUserProfile>) => void
updatePassword: (data: IUserUpdatePassword, callback?: () => void) => void
loading: boolean
error: ErrorData
setError: (error: ErrorData) => void
user: ICurrentUser | undefined;
login: (data: IUserLoginData, callback?: DataCallback) => void;
logout: (callback?: DataCallback) => void;
signup: (data: IUserSignupData, callback?: DataCallback<IUserProfile>) => void;
updatePassword: (data: IUserUpdatePassword, callback?: () => void) => void;
loading: boolean;
error: ErrorData;
setError: (error: ErrorData) => void;
}
const AuthContext = createContext<IAuthContext | null>(null);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error(
'useAuth has to be used within <AuthState.Provider>'
);
throw new Error('useAuth has to be used within <AuthState.Provider>');
}
return context;
}
};
interface AuthStateProps {
children: React.ReactNode
children: React.ReactNode;
}
export const AuthState = ({ children }: AuthStateProps) => {
@ -59,7 +57,9 @@ export const AuthState = ({ children }: AuthStateProps) => {
if (callback) callback();
}
});
}, [setUser]);
},
[setUser]
);
function login(data: IUserLoginData, callback?: DataCallback) {
setError(undefined);
@ -68,7 +68,8 @@ export const AuthState = ({ children }: AuthStateProps) => {
showError: true,
setLoading: setLoading,
onError: error => setError(error),
onSuccess: newData => reload(() => {
onSuccess: newData =>
reload(() => {
if (callback) callback(newData);
})
});
@ -78,7 +79,8 @@ export const AuthState = ({ children }: AuthStateProps) => {
setError(undefined);
postLogout({
showError: true,
onSuccess: newData => reload(() => {
onSuccess: newData =>
reload(() => {
if (callback) callback(newData);
})
});
@ -91,7 +93,8 @@ export const AuthState = ({ children }: AuthStateProps) => {
showError: true,
setLoading: setLoading,
onError: error => setError(error),
onSuccess: newData => reload(() => {
onSuccess: newData =>
reload(() => {
users.push(newData as IUserInfo);
if (callback) callback(newData);
})
@ -106,20 +109,22 @@ export const AuthState = ({ children }: AuthStateProps) => {
showError: true,
setLoading: setLoading,
onError: error => setError(error),
onSuccess: () => reload(() => {
onSuccess: () =>
reload(() => {
if (callback) callback();
})
});
}, [reload]);
},
[reload]
);
useLayoutEffect(() => {
reload();
}, [reload])
}, [reload]);
return (
<AuthContext.Provider
value={{ user, login, logout, signup, loading, error, setError, updatePassword }}
>
<AuthContext.Provider value={{ user, login, logout, signup, loading, error, setError, updatePassword }}>
{children}
</AuthContext.Provider>);
</AuthContext.Provider>
);
};

View File

@ -8,41 +8,47 @@ import { matchLibraryItem } from '@/models/libraryAPI';
import { ILibraryFilter } from '@/models/miscellaneous';
import { IRSForm, IRSFormCreateData, IRSFormData } from '@/models/rsform';
import { loadRSFormData } from '@/models/rsformAPI';
import { DataCallback, deleteLibraryItem, getLibrary, getRSFormDetails, getTemplates, postCloneLibraryItem, postNewRSForm } from '@/utils/backendAPI';
import {
DataCallback,
deleteLibraryItem,
getLibrary,
getRSFormDetails,
getTemplates,
postCloneLibraryItem,
postNewRSForm
} from '@/utils/backendAPI';
import { useAuth } from './AuthContext';
interface ILibraryContext {
items: ILibraryItem[]
templates: ILibraryItem[]
loading: boolean
processing: boolean
error: ErrorData
setError: (error: ErrorData) => void
items: ILibraryItem[];
templates: ILibraryItem[];
loading: boolean;
processing: boolean;
error: ErrorData;
setError: (error: ErrorData) => void;
applyFilter: (params: ILibraryFilter) => ILibraryItem[]
retrieveTemplate: (templateID: number, callback: (schema: IRSForm) => void) => void
createItem: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void
cloneItem: (target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => void
destroyItem: (target: number, callback?: () => void) => void
applyFilter: (params: ILibraryFilter) => ILibraryItem[];
retrieveTemplate: (templateID: number, callback: (schema: IRSForm) => void) => void;
createItem: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void;
cloneItem: (target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => void;
destroyItem: (target: number, callback?: () => void) => void;
localUpdateItem: (data: ILibraryItem) => void
localUpdateTimestamp: (target: number) => void
localUpdateItem: (data: ILibraryItem) => void;
localUpdateTimestamp: (target: number) => void;
}
const LibraryContext = createContext<ILibraryContext | null>(null)
const LibraryContext = createContext<ILibraryContext | null>(null);
export const useLibrary = (): ILibraryContext => {
const context = useContext(LibraryContext);
if (context === null) {
throw new Error(
'useLibrary has to be used within <LibraryState.Provider>'
);
throw new Error('useLibrary has to be used within <LibraryState.Provider>');
}
return context;
}
};
interface LibraryStateProps {
children: React.ReactNode
children: React.ReactNode;
}
export const LibraryState = ({ children }: LibraryStateProps) => {
@ -77,7 +83,9 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
result = result.filter(item => matchLibraryItem(item, params.query!));
}
return result;
}, [items, user]);
},
[items, user]
);
const retrieveTemplate = useCallback(
(templateID: number, callback: (schema: IRSForm) => void) => {
@ -93,14 +101,15 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
onError: error => setError(error),
onSuccess: data => {
const schema = loadRSFormData(data);
setCachedTemplates(prev => ([...prev, schema]));
setCachedTemplates(prev => [...prev, schema]);
callback(schema);
}
});
}, [cachedTemplates]);
},
[cachedTemplates]
);
const reload = useCallback(
(callback?: () => void) => {
const reload = useCallback((callback?: () => void) => {
setItems([]);
setError(undefined);
getLibrary({
@ -128,7 +137,9 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
(data: ILibraryItem) => {
const libraryItem = items.find(item => item.id === data.id);
if (libraryItem) Object.assign(libraryItem, data);
}, [items]);
},
[items]
);
const localUpdateTimestamp = useCallback(
(target: number) => {
@ -136,7 +147,9 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
if (libraryItem) {
libraryItem.time_update = Date();
}
}, [items]);
},
[items]
);
const createItem = useCallback(
(data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => {
@ -146,57 +159,82 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
showError: true,
setLoading: setProcessing,
onError: error => setError(error),
onSuccess: newSchema => reload(() => {
onSuccess: newSchema =>
reload(() => {
if (user && !user.subscriptions.includes(newSchema.id)) {
user.subscriptions.push(newSchema.id);
}
if (callback) callback(newSchema);
})
});
}, [reload, user]);
},
[reload, user]
);
const destroyItem = useCallback(
(target: number, callback?: () => void) => {
setError(undefined)
setError(undefined);
deleteLibraryItem(String(target), {
showError: true,
setLoading: setProcessing,
onError: error => setError(error),
onSuccess: () => reload(() => {
onSuccess: () =>
reload(() => {
if (user && user.subscriptions.includes(target)) {
user.subscriptions.splice(user.subscriptions.findIndex(item => item === target), 1);
user.subscriptions.splice(
user.subscriptions.findIndex(item => item === target),
1
);
}
if (callback) callback();
})
});
}, [setError, reload, user]);
},
[setError, reload, user]
);
const cloneItem = useCallback(
(target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => {
if (!user) {
return;
}
setError(undefined)
setError(undefined);
postCloneLibraryItem(String(target), {
data: data,
showError: true,
setLoading: setProcessing,
onError: error => setError(error),
onSuccess: newSchema => reload(() => {
onSuccess: newSchema =>
reload(() => {
if (user && !user.subscriptions.includes(newSchema.id)) {
user.subscriptions.push(newSchema.id);
}
if (callback) callback(newSchema);
})
});
}, [reload, setError, user]);
},
[reload, setError, user]
);
return (
<LibraryContext.Provider value={{
items, templates, loading, processing, error, setError,
applyFilter, createItem, cloneItem, destroyItem, retrieveTemplate,
localUpdateItem, localUpdateTimestamp
}}>
<LibraryContext.Provider
value={{
items,
templates,
loading,
processing,
error,
setError,
applyFilter,
createItem,
cloneItem,
destroyItem,
retrieveTemplate,
localUpdateItem,
localUpdateTimestamp
}}
>
{children}
</LibraryContext.Provider>);
}
</LibraryContext.Provider>
);
};

Some files were not shown because too many files have changed in this diff Show More