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, ".mypy_cache/": true,
".pytest_cache/": true ".pytest_cache/": true
}, },
"python.testing.unittestArgs": [ "python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test*.py"],
"-v",
"-s",
"./tests",
"-p",
"test*.py"
],
"python.testing.pytestEnabled": false, "python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true, "python.testing.unittestEnabled": true,
"eslint.workingDirectories": [ "eslint.workingDirectories": [
@ -57,6 +51,7 @@
"datv", "datv",
"Debool", "Debool",
"Decart", "Decart",
"Downvote",
"EMPTYSET", "EMPTYSET",
"exteor", "exteor",
"femn", "femn",
@ -106,6 +101,7 @@
"tanstack", "tanstack",
"toastify", "toastify",
"tooltipic", "tooltipic",
"Upvote",
"Viewset", "Viewset",
"viewsets", "viewsets",
"wordform", "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 ( return (
<NavigationState> <NavigationState>
<div className='min-w-[30rem] clr-app antialiased'> <div className='min-w-[30rem] clr-app antialiased'>
<ConceptToaster <ConceptToaster
className='mt-[4rem] text-sm' className='mt-[4rem] text-sm' //
autoClose={3000} autoClose={3000}
draggable={false} draggable={false}
pauseOnFocusLoss={false} pauseOnFocusLoss={false}
@ -32,23 +31,22 @@ function Root() {
<Navigation /> <Navigation />
<div id={globalIDs.main_scroll} <div
className='overscroll-none min-w-fit overflow-y-auto' id={globalIDs.main_scroll}
className='overflow-y-auto overscroll-none min-w-fit'
style={{ style={{
maxHeight: viewportHeight, maxHeight: viewportHeight,
overflowY: showScroll ? 'scroll': 'auto' overflowY: showScroll ? 'scroll' : 'auto'
}} }}
> >
<main <main className='flex flex-col items-center' style={{ minHeight: mainHeight }}>
className='flex flex-col items-center'
style={{minHeight: mainHeight}}
>
<Outlet /> <Outlet />
</main> </main>
<Footer /> <Footer />
</div> </div>
</div> </div>
</NavigationState>); </NavigationState>
);
} }
const router = createBrowserRouter([ const router = createBrowserRouter([
@ -59,48 +57,46 @@ const router = createBrowserRouter([
children: [ children: [
{ {
path: '', path: '',
element: <HomePage />, element: <HomePage />
}, },
{ {
path: 'login', path: 'login',
element: <LoginPage />, element: <LoginPage />
}, },
{ {
path: 'signup', path: 'signup',
element: <RegisterPage />, element: <RegisterPage />
}, },
{ {
path: 'restore-password', path: 'restore-password',
element: <RestorePasswordPage />, element: <RestorePasswordPage />
}, },
{ {
path: 'profile', path: 'profile',
element: <UserProfilePage />, element: <UserProfilePage />
}, },
{ {
path: 'manuals', path: 'manuals',
element: <ManualsPage />, element: <ManualsPage />
}, },
{ {
path: 'library', path: 'library',
element: <LibraryPage />, element: <LibraryPage />
}, },
{ {
path: 'library/create', path: 'library/create',
element: <CreateRSFormPage />, element: <CreateRSFormPage />
}, },
{ {
path: 'rsforms/:id', path: 'rsforms/:id',
element: <RSFormPage />, element: <RSFormPage />
}, }
] ]
}, }
]); ]);
function App () { function App() {
return ( return <RouterProvider router={router} />;
<RouterProvider router={router} />
);
} }
export default App; export default App;

View File

@ -11,13 +11,10 @@ import { UsersState } from '@/context/UsersContext';
import ErrorFallback from './components/ErrorFallback'; import ErrorFallback from './components/ErrorFallback';
pdfjs.GlobalWorkerOptions.workerSrc = new URL( pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.js', import.meta.url).toString();
'pdfjs-dist/build/pdf.worker.min.js',
import.meta.url,
).toString();
const resetState = () => { 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 }) => { 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 }) { function GlobalProviders({ children }: { children: React.ReactNode }) {
return ( return (
<ErrorBoundary <ErrorBoundary

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,24 +7,16 @@ import { HelpTopic } from '@/models/miscellaneous';
import { CProps } from '../props'; import { CProps } from '../props';
import InfoTopic from './InfoTopic'; import InfoTopic from './InfoTopic';
interface HelpButtonProps interface HelpButtonProps extends CProps.Styling {
extends CProps.Styling { topic: HelpTopic;
topic: HelpTopic offset?: number;
offset?: number
} }
function HelpButton({ topic, ...restProps }: HelpButtonProps) { function HelpButton({ topic, ...restProps }: HelpButtonProps) {
return ( return (
<div <div id={`help-${topic}`} className='p-1'>
id={`help-${topic}`}
className='p-1'
>
<BiInfoCircle size='1.25rem' className='clr-text-primary' /> <BiInfoCircle size='1.25rem' className='clr-text-primary' />
<ConceptTooltip clickable <ConceptTooltip clickable anchorSelect={`#help-${topic}`} layer='z-modal-tooltip' {...restProps}>
anchorSelect={`#help-${topic}`}
layer='z-modal-tooltip'
{...restProps}
>
<div className='relative'> <div className='relative'>
<div className='absolute right-0 text-sm top-[0.4rem]'> <div className='absolute right-0 text-sm top-[0.4rem]'>
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} /> <TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
@ -32,7 +24,8 @@ function HelpButton({ topic, ...restProps }: HelpButtonProps) {
</div> </div>
<InfoTopic topic={topic} /> <InfoTopic topic={topic} />
</ConceptTooltip> </ConceptTooltip>
</div>); </div>
);
} }
export default HelpButton; export default HelpButton;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,13 +9,13 @@ const OPT_VIDEO_H = 1080;
function HelpRSLang() { function HelpRSLang() {
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const videoHeight = useMemo( const videoHeight = useMemo(() => {
() => {
const viewH = windowSize.height ?? 0; const viewH = windowSize.height ?? 0;
const viewW = windowSize.width ?? 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]); }, [windowSize]);
// prettier-ignore
return ( return (
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
<div> <div>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,16 @@
// Search new icons at https://reactsvgicons.com/ // Search new icons at https://reactsvgicons.com/
interface IconSVGProps { interface IconSVGProps {
viewBox: string viewBox: string;
size?: string size?: string;
className?: string className?: string;
props?: React.SVGProps<SVGSVGElement> props?: React.SVGProps<SVGSVGElement>;
children: React.ReactNode children: React.ReactNode;
} }
export interface IconProps { export interface IconProps {
size?: string size?: string;
className?: string className?: string;
} }
function IconSVG({ viewBox, size = '1.5rem', className, props, children }: IconSVGProps) { 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} {...props}
> >
{children} {children}
</svg>); </svg>
);
} }
export function EducationIcon(props: IconProps) { export function EducationIcon(props: IconProps) {
@ -46,11 +47,7 @@ export function InDoorIcon(props: IconProps) {
export function CheckboxCheckedIcon() { export function CheckboxCheckedIcon() {
return ( return (
<svg <svg className='w-3 h-3' viewBox='0 0 512 512' fill='#ffffff'>
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' /> <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> </svg>
); );
@ -58,11 +55,7 @@ export function CheckboxCheckedIcon() {
export function CheckboxNullIcon() { export function CheckboxNullIcon() {
return ( return (
<svg <svg className='w-3 h-3' viewBox='0 0 16 16' fill='#ffffff'>
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' /> <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> </svg>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,22 +16,17 @@ function UserMenu() {
const navigateLogin = () => router.push('/login'); const navigateLogin = () => router.push('/login');
return ( return (
<div ref={menu.ref} className='h-full'> <div ref={menu.ref} className='h-full'>
{!user ? {!user ? (
<NavigationButton <NavigationButton
title='Перейти на страницу логина' title='Перейти на страницу логина'
icon={<InDoorIcon size='1.5rem' className='clr-text-primary' />} icon={<InDoorIcon size='1.5rem' className='clr-text-primary' />}
onClick={navigateLogin} 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; 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 { ccBracketMatching } from './bracketMatching';
import { RSLanguage } from './rslang'; import { RSLanguage } from './rslang';
import { getSymbolSubstitute,RSTextWrapper } from './textEditing'; import { getSymbolSubstitute, RSTextWrapper } from './textEditing';
import { rsHoverTooltip } from './tooltip'; import { rsHoverTooltip } from './tooltip';
const editorSetup: BasicSetupOptions = { const editorSetup: BasicSetupOptions = {
@ -46,33 +46,29 @@ const editorSetup: BasicSetupOptions = {
}; };
interface RSInputProps interface RSInputProps
extends Pick<ReactCodeMirrorProps, extends Pick<
'id' | 'height' | 'minHeight' | 'maxHeight' | 'value' | ReactCodeMirrorProps,
'onFocus' | 'onBlur' | 'placeholder' | 'style' | 'className' 'id' | 'height' | 'minHeight' | 'maxHeight' | 'value' | 'onFocus' | 'onBlur' | 'placeholder' | 'style' | 'className'
> { > {
label?: string label?: string;
disabled?: boolean disabled?: boolean;
noTooltip?: boolean noTooltip?: boolean;
onChange?: (newValue: string) => void onChange?: (newValue: string) => void;
onAnalyze?: () => void onAnalyze?: () => void;
} }
const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>( 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 { darkMode, colors } = useConceptTheme();
const { schema } = useRSForm(); const { schema } = useRSForm();
const internalRef = useRef<ReactCodeMirrorRef>(null); const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]); 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( const customTheme: Extension = useMemo(
() => createTheme({ () =>
createTheme({
theme: darkMode ? 'dark' : 'light', theme: darkMode ? 'dark' : 'light',
settings: { settings: {
fontFamily: 'inherit', fontFamily: 'inherit',
@ -86,19 +82,23 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
{ tag: tags.propertyName, color: colors.fgTeal }, // Radical { tag: tags.propertyName, color: colors.fgTeal }, // Radical
{ tag: tags.keyword, color: colors.fgBlue }, // keywords { tag: tags.keyword, color: colors.fgBlue }, // keywords
{ tag: tags.literal, color: colors.fgBlue }, // literals { 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.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( const editorExtensions = useMemo(
() => [ () => [
EditorView.lineWrapping, EditorView.lineWrapping,
RSLanguage, RSLanguage,
ccBracketMatching(darkMode), ccBracketMatching(darkMode),
... noTooltip ? [] : [rsHoverTooltip(schema?.items || [])], ...(noTooltip ? [] : [rsHoverTooltip(schema?.items || [])])
], [darkMode, schema?.items, noTooltip]); ],
[darkMode, schema?.items, noTooltip]
);
const handleInput = useCallback( const handleInput = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
@ -123,19 +123,15 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
}, [thisRef, onAnalyze]); },
[thisRef, onAnalyze]
);
return ( return (
<div <div className={clsx('flex flex-col gap-2', className, cursor)} style={style}>
className={clsx( <Label text={label} htmlFor={id} />
'flex flex-col gap-2', <CodeMirror
className, id={id}
cursor
)}
style={style}
>
<Label text={label} htmlFor={id}/>
<CodeMirror id={id}
ref={thisRef} ref={thisRef}
basicSetup={editorSetup} basicSetup={editorSetup}
theme={customTheme} theme={customTheme}
@ -146,7 +142,9 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
onKeyDown={handleInput} onKeyDown={handleInput}
{...restProps} {...restProps}
/> />
</div>); </div>
}); );
}
);
export default RSInput; export default RSInput;

View File

@ -3,8 +3,8 @@ import { Decoration, EditorView } from '@codemirror/view';
import { bracketsDarkT, bracketsLightT } from '@/utils/color'; import { bracketsDarkT, bracketsLightT } from '@/utils/color';
const matchingMark = Decoration.mark({class: 'cc-matchingBracket'}); const matchingMark = Decoration.mark({ class: 'cc-matchingBracket' });
const nonMatchingMark = Decoration.mark({class: 'cc-nonmatchingBracket'}); const nonMatchingMark = Decoration.mark({ class: 'cc-nonmatchingBracket' });
function bracketRender(match: MatchResult) { function bracketRender(match: MatchResult) {
const decorations = []; const decorations = [];
@ -24,7 +24,7 @@ export function ccBracketMatching(darkMode: boolean) {
return [ return [
bracketMatching({ bracketMatching({
renderMatch: bracketRender, renderMatch: bracketRender,
brackets:'{}[]()' brackets: '{}[]()'
}), }),
darkMode ? darkTheme : lightTheme 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({ export const highlighting = styleTags({
Index: tags.unit, 'Index': tags.unit,
ComplexIndex: tags.unit, 'ComplexIndex': tags.unit,
Literal: tags.literal, 'Literal': tags.literal,
Radical: tags.propertyName, 'Radical': tags.propertyName,
Function: tags.name, 'Function': tags.name,
Predicate: tags.name, 'Predicate': tags.name,
Global: tags.name, 'Global': tags.name,
Local: tags.variableName, 'Local': tags.variableName,
TextFunction: tags.keyword, 'TextFunction': tags.keyword,
Filter: tags.keyword, 'Filter': tags.keyword,
PrefixR: tags.controlKeyword, 'PrefixR': tags.controlKeyword,
PrefixI: tags.controlKeyword, 'PrefixI': tags.controlKeyword,
PrefixD: tags.controlKeyword, 'PrefixD': tags.controlKeyword,
"{": tags.brace, '{': tags.brace,
"}": tags.brace, '}': tags.brace,
"|": 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 { parser } from './parser';
import { Function, Global, Predicate } from './parser.terms'; import { Function, Global, Predicate } from './parser.terms';
export const GlobalTokens: number[] = [ export const GlobalTokens: number[] = [Global, Function, Predicate];
Global, Function, Predicate
]
export const RSLanguage = LRLanguage.define({ export const RSLanguage = LRLanguage.define({
parser: parser, parser: parser,

View File

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

View File

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

View File

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

View File

@ -13,12 +13,11 @@ const testData = [
['@{-1| черный }', '[Text[RefSyntactic[Offset][Nominal]]]'], ['@{-1| черный }', '[Text[RefSyntactic[Offset][Nominal]]]'],
['@{-100| черный слон }', '[Text[RefSyntactic[Offset][Nominal]]]'], ['@{-100| черный слон }', '[Text[RefSyntactic[Offset][Nominal]]]'],
['@{X1|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]'], ['@{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', () => { describe('Testing NaturalParser', () => {
it.each(testData)('Parse %p', it.each(testData)('Parse %p', (input: string, expectedTree: string) => {
(input: string, expectedTree: string) => {
// NOTE: use strict parser to determine exact error position // NOTE: use strict parser to determine exact error position
// const tree = parser.configure({strict: true}).parse(input); // const tree = parser.configure({strict: true}).parse(input);
const tree = parser.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 { Extension } from '@codemirror/state';
import { hoverTooltip } from '@codemirror/view'; import { hoverTooltip } from '@codemirror/view';
import { parseEntityReference, parseSyntacticReference } from '@/models/languageAPI'; import { parseEntityReference, parseSyntacticReference } from '@/models/languageAPI';
import { IConstituenta } from '@/models/rsform'; 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 { IColorTheme } from '@/utils/color';
import { ReferenceTokens } from './parse'; import { ReferenceTokens } from './parse';
import { RefEntity, RefSyntactic } from './parse/parser.terms'; import { RefEntity, RefSyntactic } from './parse/parser.terms';
export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme) => { export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme) => {
return hoverTooltip( return hoverTooltip((view, pos) => {
(view, pos) => {
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), ReferenceTokens); const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), ReferenceTokens);
if (nodes.length !== 1) { if (nodes.length !== 1) {
return null; return null;
@ -28,7 +32,7 @@ export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme)
end: end, end: end,
above: false, above: false,
create: () => domTooltipEntityReference(ref, cst, colors) create: () => domTooltipEntityReference(ref, cst, colors)
} };
} else if (nodes[0].type.id === RefSyntactic) { } else if (nodes[0].type.id === RefSyntactic) {
const ref = parseSyntacticReference(text); const ref = parseSyntacticReference(text);
let masterText: string | undefined = undefined; let masterText: string | undefined = undefined;
@ -50,12 +54,12 @@ export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme)
end: end, end: end,
above: false, above: false,
create: () => domTooltipSyntacticReference(ref, masterText) create: () => domTooltipSyntacticReference(ref, masterText)
} };
} else { } else {
return null; return null;
} }
}); });
} };
export function refsHoverTooltip(items: IConstituenta[], colors: IColorTheme): Extension { export function refsHoverTooltip(items: IConstituenta[], colors: IColorTheme): Extension {
return [globalsHoverTooltip(items, colors)]; return [globalsHoverTooltip(items, colors)];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,39 +2,40 @@
import { HTMLMotionProps } from 'framer-motion'; import { HTMLMotionProps } from 'framer-motion';
export namespace CProps { export namespace CProps {
export type Control = {
title?: string;
disabled?: boolean;
noBorder?: boolean;
noOutline?: boolean;
};
export type Control = { export type Styling = {
title?: string style?: React.CSSProperties;
disabled?: boolean className?: string;
noBorder?: boolean };
noOutline?: boolean
}
export type Styling = { export type Editor = Control & {
style?: React.CSSProperties label?: string;
className?: string };
}
export type Editor = Control & { export type Colors = {
label?: string colors?: string;
} };
export type Colors = { export type Div = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
colors?: string export type Button = Omit<
}
export type Div = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
export type Button = Omit<
React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>,
'children' | 'type' 'children' | 'type'
>; >;
export type Label = Omit< export type Label = Omit<
React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>, React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>,
'children' 'children'
>; >;
export type TextArea = React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>; export type TextArea = React.DetailedHTMLProps<
export type Input = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>; React.TextareaHTMLAttributes<HTMLTextAreaElement>,
HTMLTextAreaElement
export type AnimatedButton = Omit<HTMLMotionProps<'button'>, 'type'>; >;
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'; import { UserAccessMode } from '@/models/miscellaneous';
interface IAccessModeContext { interface IAccessModeContext {
mode: UserAccessMode mode: UserAccessMode;
setMode: React.Dispatch<React.SetStateAction<UserAccessMode>> setMode: React.Dispatch<React.SetStateAction<UserAccessMode>>;
} }
const AccessContext = createContext<IAccessModeContext | null>(null); const AccessContext = createContext<IAccessModeContext | null>(null);
export const useAccessMode = () => { export const useAccessMode = () => {
const context = useContext(AccessContext); const context = useContext(AccessContext);
if (!context) { if (!context) {
throw new Error( throw new Error('useAccessMode has to be used within <AccessModeState.Provider>');
'useAccessMode has to be used within <AccessModeState.Provider>'
);
} }
return context; return context;
} };
interface AccessModeStateProps { interface AccessModeStateProps {
children: React.ReactNode children: React.ReactNode;
} }
export const AccessModeState = ({ children }: AccessModeStateProps) => { export const AccessModeState = ({ children }: AccessModeStateProps) => {
const [mode, setMode] = useState<UserAccessMode>(UserAccessMode.READER); const [mode, setMode] = useState<UserAccessMode>(UserAccessMode.READER);
return ( return <AccessContext.Provider value={{ mode, setMode }}>{children}</AccessContext.Provider>;
<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 { IUserProfile } from '@/models/library';
import { IUserInfo } from '@/models/library'; import { IUserInfo } from '@/models/library';
import { IUserUpdatePassword } 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'; import { useUsers } from './UsersContext';
interface IAuthContext { interface IAuthContext {
user: ICurrentUser | undefined user: ICurrentUser | undefined;
login: (data: IUserLoginData, callback?: DataCallback) => void login: (data: IUserLoginData, callback?: DataCallback) => void;
logout: (callback?: DataCallback) => void logout: (callback?: DataCallback) => void;
signup: (data: IUserSignupData, callback?: DataCallback<IUserProfile>) => void signup: (data: IUserSignupData, callback?: DataCallback<IUserProfile>) => void;
updatePassword: (data: IUserUpdatePassword, callback?: () => void) => void updatePassword: (data: IUserUpdatePassword, callback?: () => void) => void;
loading: boolean loading: boolean;
error: ErrorData error: ErrorData;
setError: (error: ErrorData) => void setError: (error: ErrorData) => void;
} }
const AuthContext = createContext<IAuthContext | null>(null); const AuthContext = createContext<IAuthContext | null>(null);
export const useAuth = () => { export const useAuth = () => {
const context = useContext(AuthContext); const context = useContext(AuthContext);
if (!context) { if (!context) {
throw new Error( throw new Error('useAuth has to be used within <AuthState.Provider>');
'useAuth has to be used within <AuthState.Provider>'
);
} }
return context; return context;
} };
interface AuthStateProps { interface AuthStateProps {
children: React.ReactNode children: React.ReactNode;
} }
export const AuthState = ({ children }: AuthStateProps) => { export const AuthState = ({ children }: AuthStateProps) => {
@ -59,7 +57,9 @@ export const AuthState = ({ children }: AuthStateProps) => {
if (callback) callback(); if (callback) callback();
} }
}); });
}, [setUser]); },
[setUser]
);
function login(data: IUserLoginData, callback?: DataCallback) { function login(data: IUserLoginData, callback?: DataCallback) {
setError(undefined); setError(undefined);
@ -68,7 +68,8 @@ export const AuthState = ({ children }: AuthStateProps) => {
showError: true, showError: true,
setLoading: setLoading, setLoading: setLoading,
onError: error => setError(error), onError: error => setError(error),
onSuccess: newData => reload(() => { onSuccess: newData =>
reload(() => {
if (callback) callback(newData); if (callback) callback(newData);
}) })
}); });
@ -78,7 +79,8 @@ export const AuthState = ({ children }: AuthStateProps) => {
setError(undefined); setError(undefined);
postLogout({ postLogout({
showError: true, showError: true,
onSuccess: newData => reload(() => { onSuccess: newData =>
reload(() => {
if (callback) callback(newData); if (callback) callback(newData);
}) })
}); });
@ -91,7 +93,8 @@ export const AuthState = ({ children }: AuthStateProps) => {
showError: true, showError: true,
setLoading: setLoading, setLoading: setLoading,
onError: error => setError(error), onError: error => setError(error),
onSuccess: newData => reload(() => { onSuccess: newData =>
reload(() => {
users.push(newData as IUserInfo); users.push(newData as IUserInfo);
if (callback) callback(newData); if (callback) callback(newData);
}) })
@ -106,20 +109,22 @@ export const AuthState = ({ children }: AuthStateProps) => {
showError: true, showError: true,
setLoading: setLoading, setLoading: setLoading,
onError: error => setError(error), onError: error => setError(error),
onSuccess: () => reload(() => { onSuccess: () =>
reload(() => {
if (callback) callback(); if (callback) callback();
}) })
}); });
}, [reload]); },
[reload]
);
useLayoutEffect(() => { useLayoutEffect(() => {
reload(); reload();
}, [reload]) }, [reload]);
return ( return (
<AuthContext.Provider <AuthContext.Provider value={{ user, login, logout, signup, loading, error, setError, updatePassword }}>
value={{ user, login, logout, signup, loading, error, setError, updatePassword }}
>
{children} {children}
</AuthContext.Provider>); </AuthContext.Provider>
);
}; };

View File

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

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