Refactoring: centralize internal URLs + animation fixes

This commit is contained in:
IRBorisov 2024-04-01 21:45:10 +03:00
parent aff116abbc
commit 9fe73a607a
34 changed files with 265 additions and 190 deletions

View File

@ -1,7 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useConceptOptions } from '@/context/OptionsContext'; import { useConceptOptions } from '@/context/OptionsContext';
import { urls } from '@/utils/constants'; import { external_urls } from '@/utils/constants';
import TextURL from '../components/ui/TextURL'; import TextURL from '../components/ui/TextURL';
@ -22,7 +22,7 @@ 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={external_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>

View File

@ -8,6 +8,7 @@ import { useConceptNavigation } from '@/context/NavigationContext';
import { useConceptOptions } from '@/context/OptionsContext'; import { useConceptOptions } from '@/context/OptionsContext';
import { animateNavigation } from '@/styling/animations'; import { animateNavigation } from '@/styling/animations';
import { urls } from '../urls';
import Logo from './Logo'; import Logo from './Logo';
import NavigationButton from './NavigationButton'; import NavigationButton from './NavigationButton';
import ToggleNavigationButton from './ToggleNavigationButton'; import ToggleNavigationButton from './ToggleNavigationButton';
@ -17,10 +18,10 @@ function Navigation() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { noNavigationAnimation } = useConceptOptions(); const { noNavigationAnimation } = useConceptOptions();
const navigateHome = () => router.push('/'); const navigateHome = () => router.push(urls.home);
const navigateLibrary = () => router.push('/library'); const navigateLibrary = () => router.push(urls.library);
const navigateHelp = () => router.push('/manuals'); const navigateHelp = () => router.push(urls.manuals);
const navigateCreateNew = () => router.push('/library/create'); const navigateCreateNew = () => router.push(urls.create_schema);
return ( return (
<nav <nav

View File

@ -6,6 +6,8 @@ import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { useConceptOptions } from '@/context/OptionsContext'; import { useConceptOptions } from '@/context/OptionsContext';
import { urls } from '../urls';
interface UserDropdownProps { interface UserDropdownProps {
isOpen: boolean; isOpen: boolean;
hideDropdown: () => void; hideDropdown: () => void;
@ -18,12 +20,12 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
function navigateProfile() { function navigateProfile() {
hideDropdown(); hideDropdown();
router.push('/profile'); router.push(urls.profile);
} }
function logoutAndRedirect() { function logoutAndRedirect() {
hideDropdown(); hideDropdown();
logout(() => router.push('/login/')); logout(() => router.push(urls.login));
} }
function handleToggleDarkMode() { function handleToggleDarkMode() {

View File

@ -5,6 +5,7 @@ import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { urls } from '../urls';
import NavigationButton from './NavigationButton'; import NavigationButton from './NavigationButton';
import UserDropdown from './UserDropdown'; import UserDropdown from './UserDropdown';
@ -13,7 +14,7 @@ function UserMenu() {
const { user } = useAuth(); const { user } = useAuth();
const menu = useDropdown(); const menu = useDropdown();
const navigateLogin = () => router.push('/login'); const navigateLogin = () => router.push(urls.login);
return ( return (
<div ref={menu.ref} className='h-full'> <div ref={menu.ref} className='h-full'>
{!user ? ( {!user ? (

View File

@ -13,6 +13,7 @@ import RSFormPage from '@/pages/RSFormPage';
import UserProfilePage from '@/pages/UserProfilePage'; import UserProfilePage from '@/pages/UserProfilePage';
import ApplicationLayout from './ApplicationLayout'; import ApplicationLayout from './ApplicationLayout';
import { routes } from './urls';
export const Router = createBrowserRouter([ export const Router = createBrowserRouter([
{ {
@ -25,40 +26,40 @@ export const Router = createBrowserRouter([
element: <HomePage /> element: <HomePage />
}, },
{ {
path: 'login', path: routes.login,
element: <LoginPage /> element: <LoginPage />
}, },
{ {
path: 'signup', path: routes.signup,
element: <RegisterPage /> element: <RegisterPage />
}, },
{ {
path: 'restore-password', path: routes.profile,
element: <RestorePasswordPage />
},
{
path: 'password-change',
element: <PasswordChangePage />
},
{
path: 'profile',
element: <UserProfilePage /> element: <UserProfilePage />
}, },
{ {
path: 'manuals', path: routes.restore_password,
element: <ManualsPage /> element: <RestorePasswordPage />
}, },
{ {
path: 'library', path: routes.password_change,
element: <PasswordChangePage />
},
{
path: routes.library,
element: <LibraryPage /> element: <LibraryPage />
}, },
{ {
path: 'library/create', path: routes.create_schema,
element: <CreateRSFormPage /> element: <CreateRSFormPage />
}, },
{ {
path: 'rsforms/:id', path: `${routes.rsforms}/:id`,
element: <RSFormPage /> element: <RSFormPage />
},
{
path: routes.manuals,
element: <ManualsPage />
} }
] ]
} }

View File

@ -0,0 +1,49 @@
/**
* Module: Internal navigation constants.
*/
/**
* Routes.
*/
export const routes = {
login: 'login',
signup: 'signup',
profile: 'profile',
restore_password: 'restore-password',
password_change: 'password-change',
library: 'library',
create_schema: 'library/create',
manuals: 'manuals',
help: 'manuals',
rsforms: 'rsforms'
};
interface SchemaProps {
id: number | string;
tab: number;
version?: number | string;
active?: number | string;
}
/**
* Internal navigation URLs.
*/
export const urls = {
home: '/',
login: `/${routes.login}`,
login_hint: (userName: string) => `/login?username=${userName}`,
profile: `/${routes.profile}`,
signup: `/${routes.signup}`,
library: `/${routes.library}`,
library_filter: (strategy: string) => `/library?filter=${strategy}`,
create_schema: `/${routes.create_schema}`,
manuals: `/${routes.manuals}`,
help_topic: (topic: string) => `/manuals?topic=${topic}`,
schema: (id: number | string, version?: number | string) =>
`/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''),
schema_props: ({ id, tab, version, active }: SchemaProps) => {
const versionStr = version !== undefined ? `v=${version}&` : '';
const activeStr = active !== undefined ? `&active=${active}` : '';
return `/rsforms/${id}?${versionStr}tab=${tab}${activeStr}`;
}
};

View File

@ -10,8 +10,8 @@ import { AnimatePresence } from 'framer-motion';
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react'; import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import { useRSForm } from '@/context/RSFormContext';
import { useConceptOptions } from '@/context/OptionsContext'; import { useConceptOptions } from '@/context/OptionsContext';
import { useRSForm } from '@/context/RSFormContext';
import DlgEditReference from '@/dialogs/DlgEditReference'; import DlgEditReference from '@/dialogs/DlgEditReference';
import { ReferenceType } from '@/models/language'; import { ReferenceType } from '@/models/language';
import { IConstituenta } from '@/models/rsform'; import { IConstituenta } from '@/models/rsform';
@ -163,7 +163,7 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
); );
return ( return (
<> <div className={clsx('flex flex-col gap-2', cursor)}>
<AnimatePresence> <AnimatePresence>
{showEditor ? ( {showEditor ? (
<DlgEditReference <DlgEditReference
@ -180,27 +180,24 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
/> />
) : null} ) : null}
</AnimatePresence> </AnimatePresence>
<Label text={label} />
<div className={clsx('flex flex-col gap-2', cursor)}> <CodeMirror
<Label text={label} /> id={id}
<CodeMirror ref={thisRef}
id={id} basicSetup={editorSetup}
ref={thisRef} theme={customTheme}
basicSetup={editorSetup} extensions={editorExtensions}
theme={customTheme} value={isFocused ? value : value !== initialValue || showEditor ? value : resolved}
extensions={editorExtensions} indentWithTab={false}
value={isFocused ? value : value !== initialValue || showEditor ? value : resolved} onChange={handleChange}
indentWithTab={false} editable={!disabled}
onChange={handleChange} onKeyDown={handleInput}
editable={!disabled} onFocus={handleFocusIn}
onKeyDown={handleInput} onBlur={handleFocusOut}
onFocus={handleFocusIn} // spellCheck= // TODO: figure out while automatic spellcheck doesn't work or implement with extension
onBlur={handleFocusOut} {...restProps}
// spellCheck= // TODO: figure out while automatic spellcheck doesn't work or implement with extension />
{...restProps} </div>
/>
</div>
</>
); );
} }
); );

View File

@ -1,7 +1,7 @@
import axios, { type AxiosError } from 'axios'; import axios, { type AxiosError } from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { urls } from '@/utils/constants'; import { external_urls } from '@/utils/constants';
import { isResponseHtml } from '@/utils/utils'; import { isResponseHtml } from '@/utils/utils';
import PrettyJson from '../ui/PrettyJSON'; import PrettyJson from '../ui/PrettyJSON';
@ -72,7 +72,7 @@ function InfoError({ error }: InfoErrorProps) {
> >
<p className='font-normal clr-text-default'> <p className='font-normal clr-text-default'>
Пожалуйста сделайте скриншот и отправьте вместе с описанием ситуации на почту{' '} Пожалуйста сделайте скриншот и отправьте вместе с описанием ситуации на почту{' '}
<TextURL href={urls.mail_portal} text='portal@acconcept.ru' /> <TextURL href={external_urls.mail_portal} text='portal@acconcept.ru' />
<br /> <br />
Для продолжения работы перезагрузите страницу Для продолжения работы перезагрузите страницу
</p> </p>

View File

@ -1,5 +1,5 @@
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import { urls } from '@/utils/constants'; import { external_urls } from '@/utils/constants';
function HelpAPI() { function HelpAPI() {
return ( return (
@ -8,10 +8,10 @@ function HelpAPI() {
<p>В качестве программного интерфейса сервера используется REST API, реализованный с помощью Django.</p> <p>В качестве программного интерфейса сервера используется REST API, реализованный с помощью Django.</p>
<p>На данный момент API находится в разработке, поэтому поддержка внешних запросов не производится.</p> <p>На данный момент API находится в разработке, поэтому поддержка внешних запросов не производится.</p>
<p> <p>
С описанием интерфейса можно ознакомиться <TextURL text='по ссылке' href={urls.restAPI} />. С описанием интерфейса можно ознакомиться <TextURL text='по ссылке' href={external_urls.restAPI} />.
</p> </p>
<p> <p>
<TextURL text='Принять участие в разработке' href={urls.git_repo} /> <TextURL text='Принять участие в разработке' href={external_urls.git_repo} />
</p> </p>
</div> </div>
); );

View File

@ -1,5 +1,5 @@
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import { urls } from '@/utils/constants'; import { external_urls } from '@/utils/constants';
function HelpExteor() { function HelpExteor() {
// prettier-ignore // prettier-ignore
@ -9,7 +9,7 @@ function HelpExteor() {
<p>Экстеор 4.9 редактор текстов систем понятий эксплицированных в родах структур</p> <p>Экстеор 4.9 редактор текстов систем понятий эксплицированных в родах структур</p>
<p>Портал превосходит Экстеор в части редактирования экспликаций, но функции синтеза и вычисления интерпретации пока доступны только в Экстеоре. Также следует использовать Экстеор для выгрузки экспликаций в Word для последующей печати</p> <p>Портал превосходит Экстеор в части редактирования экспликаций, но функции синтеза и вычисления интерпретации пока доступны только в Экстеоре. Также следует использовать Экстеор для выгрузки экспликаций в Word для последующей печати</p>
<p>Экстеор доступен на операционной системы Windows 10+</p> <p>Экстеор доступен на операционной системы Windows 10+</p>
<p>Скачать установщик: <TextURL href={urls.exteor64} text='64bit'/> | <TextURL href={urls.exteor32} text='32bit'/></p> <p>Скачать установщик: <TextURL href={external_urls.exteor64} text='64bit'/> | <TextURL href={external_urls.exteor32} text='32bit'/></p>
<h2>Основные функции</h2> <h2>Основные функции</h2>
<li>Работа с РС-формой системы понятий</li> <li>Работа с РС-формой системы понятий</li>
<li>Автоматическое определение типизации выражений</li> <li>Автоматическое определение типизации выражений</li>

View File

@ -1,5 +1,5 @@
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import { urls } from '@/utils/constants'; import { external_urls } from '@/utils/constants';
function HelpMain() { function HelpMain() {
// prettier-ignore // prettier-ignore
@ -16,8 +16,8 @@ function HelpMain() {
<p>В меню пользователя (правый угол) доступно редактирование пользователя и изменение цветовой темы</p> <p>В меню пользователя (правый угол) доступно редактирование пользователя и изменение цветовой темы</p>
<h2>Поддержка</h2> <h2>Поддержка</h2>
<p>Портал разрабатывается <TextURL text='Центром Концепт' href={urls.concept}/> и является проектом с открытым исходным кодом, доступным на <TextURL text='Github' href={urls.git_repo}/></p> <p>Портал разрабатывается <TextURL text='Центром Концепт' href={external_urls.concept}/> и является проектом с открытым исходным кодом, доступным на <TextURL text='Github' href={external_urls.git_repo}/></p>
<p>Ваши пожелания по доработке, найденные ошибки и иные предложения можно направлять по email: <TextURL href={urls.mail_portal} text='portal@acconcept.ru'/></p> <p>Ваши пожелания по доработке, найденные ошибки и иные предложения можно направлять по email: <TextURL href={external_urls.mail_portal} text='portal@acconcept.ru'/></p>
</div>); </div>);
} }

View File

@ -2,7 +2,7 @@ import { useMemo } from 'react';
import EmbedYoutube from '@/components/ui/EmbedYoutube'; import EmbedYoutube from '@/components/ui/EmbedYoutube';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { urls, youtube } from '@/utils/constants'; import { external_urls, youtube } from '@/utils/constants';
const OPT_VIDEO_H = 1080; const OPT_VIDEO_H = 1080;
@ -25,9 +25,9 @@ function HelpRSLang() {
<p>Данный математический аппарат основан на аксиоматической теории множеств Цермелло-Френкеля и аппарате родов структур Н.Бурбаки.</p> <p>Данный математический аппарат основан на аксиоматической теории множеств Цермелло-Френкеля и аппарате родов структур Н.Бурбаки.</p>
<p>Для ознакомления с основами родов структур можно использовать следующие материалы:</p> <p>Для ознакомления с основами родов структур можно использовать следующие материалы:</p>
<ul> <ul>
<li>1. <a className='underline' href={urls.intro_video}>Видео: Краткое введение в мат. аппарат</a></li> <li>1. <a className='underline' href={external_urls.intro_video}>Видео: Краткое введение в мат. аппарат</a></li>
<li>2. <a className='underline' href={urls.ponomarev}>Текст: Учебник И. Н. Пономарева</a></li> <li>2. <a className='underline' href={external_urls.ponomarev}>Текст: Учебник И. Н. Пономарева</a></li>
<li>3. <a className='underline' href={urls.full_course}>Видео: лекции для 4 курса (второй семестр 2022-23 год)</a></li> <li>3. <a className='underline' href={external_urls.full_course}>Видео: лекции для 4 курса (второй семестр 2022-23 год)</a></li>
</ul> </ul>
</div> </div>
<div className='justify-center w-full'> <div className='justify-center w-full'>

View File

@ -17,13 +17,13 @@ interface DataLoaderProps extends CProps.AnimatedDiv {
function DataLoader({ id, isLoading, hasNoData, error, children, ...restProps }: DataLoaderProps) { function DataLoader({ id, isLoading, hasNoData, error, children, ...restProps }: DataLoaderProps) {
return ( return (
<AnimatePresence mode='wait'> <AnimatePresence>
{isLoading ? <Loader key={`${id}-loader`} /> : null} {isLoading ? <Loader key={`${id}-loader`} /> : null}
{error ? <InfoError key={`${id}-error`} error={error} /> : null} {error ? <InfoError key={`${id}-error`} error={error} /> : null}
<AnimateFade id={id} key={`${id}-data`} removeContent={isLoading || !!error || hasNoData} {...restProps}> <AnimateFade id={id} key={`${id}-data`} removeContent={isLoading || !!error || hasNoData} {...restProps}>
{children} {children}
</AnimateFade> </AnimateFade>
<AnimateFade id={id} key={`${id}-data`} removeContent={isLoading || !!error || !hasNoData} {...restProps}> <AnimateFade id={id} key={`${id}-no-data`} removeContent={isLoading || !!error || !hasNoData} {...restProps}>
Данные не загружены Данные не загружены
</AnimateFade> </AnimateFade>
</AnimatePresence> </AnimatePresence>

View File

@ -1,3 +1,4 @@
import { urls } from '@/app/urls';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
@ -8,7 +9,7 @@ function ExpectedAnonymous() {
const router = useConceptNavigation(); const router = useConceptNavigation();
function logoutAndRedirect() { function logoutAndRedirect() {
logout(() => router.push('/login/')); logout(() => router.push(urls.login));
} }
return ( return (

View File

@ -4,6 +4,7 @@ import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import Checkbox from '@/components/ui/Checkbox'; import Checkbox from '@/components/ui/Checkbox';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
@ -51,7 +52,7 @@ function DlgCloneLibraryItem({ hideWindow, base }: DlgCloneLibraryItemProps) {
}; };
cloneItem(base.id, data, newSchema => { cloneItem(base.id, data, newSchema => {
toast.success(`Копия создана: ${newSchema.alias}`); toast.success(`Копия создана: ${newSchema.alias}`);
router.push(`/rsforms/${newSchema.id}`); router.push(urls.schema(newSchema.id));
}); });
} }

View File

@ -41,7 +41,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
return ( return (
<AnimatePresence> <AnimatePresence>
<div className='flex items-center self-center'> <div key='dlg_cst_alias_picker' className='flex items-center self-center'>
<SelectSingle <SelectSingle
id='dlg_cst_type' id='dlg_cst_type'
placeholder='Выберите тип' placeholder='Выберите тип'
@ -61,6 +61,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
/> />
</div> </div>
<TextArea <TextArea
key='dlg_cst_term'
id='dlg_cst_term' id='dlg_cst_term'
spellCheck spellCheck
label='Термин' label='Термин'
@ -69,7 +70,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
value={state.term_raw} value={state.term_raw}
onChange={event => partialUpdate({ term_raw: event.target.value })} onChange={event => partialUpdate({ term_raw: event.target.value })}
/> />
<AnimateFade hideContent={!state.definition_formal && isElementary}> <AnimateFade key='dlg_cst_expression' hideContent={!state.definition_formal && isElementary}>
<RSInput <RSInput
id='dlg_cst_expression' id='dlg_cst_expression'
label={ label={
@ -88,7 +89,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
onChange={value => partialUpdate({ definition_formal: value })} onChange={value => partialUpdate({ definition_formal: value })}
/> />
</AnimateFade> </AnimateFade>
<AnimateFade hideContent={!state.definition_raw && isElementary}> <AnimateFade key='dlg_cst_definition' hideContent={!state.definition_raw && isElementary}>
<TextArea <TextArea
id='dlg_cst_definition' id='dlg_cst_definition'
spellCheck spellCheck
@ -101,6 +102,8 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
</AnimateFade> </AnimateFade>
{!showConvention ? ( {!showConvention ? (
<button <button
key='dlg_cst_show_comment'
id='dlg_cst_show_comment'
type='button' type='button'
className='self-start cc-label clr-text-url hover:underline' className='self-start cc-label clr-text-url hover:underline'
onClick={() => setForceComment(true)} onClick={() => setForceComment(true)}
@ -110,6 +113,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
) : null} ) : null}
<AnimateFade hideContent={!showConvention}> <AnimateFade hideContent={!showConvention}>
<TextArea <TextArea
key='dlg_cst_convention'
id='dlg_cst_convention' id='dlg_cst_convention'
spellCheck spellCheck
label={isBasic ? 'Конвенция' : 'Комментарий'} label={isBasic ? 'Конвенция' : 'Комментарий'}

View File

@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from 'react';
import { BiDownload } from 'react-icons/bi'; import { BiDownload } from 'react-icons/bi';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import InfoError from '@/components/info/InfoError'; import InfoError from '@/components/info/InfoError';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Checkbox from '@/components/ui/Checkbox'; import Checkbox from '@/components/ui/Checkbox';
@ -43,7 +44,7 @@ function CreateRSFormPage() {
if (router.canBack()) { if (router.canBack()) {
router.back(); router.back();
} else { } else {
router.push('/library'); router.push(urls.library);
} }
} }
@ -64,7 +65,7 @@ function CreateRSFormPage() {
}; };
createItem(data, newSchema => { createItem(data, newSchema => {
toast.success('Схема успешно создана'); toast.success('Схема успешно создана');
router.push(`/rsforms/${newSchema.id}`); router.push(urls.schema(newSchema.id));
}); });
} }

View File

@ -1,5 +1,6 @@
import { useLayoutEffect } from 'react'; import { useLayoutEffect } from 'react';
import { urls } from '@/app/urls';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { TIMEOUT_UI_REFRESH } from '@/utils/constants'; import { TIMEOUT_UI_REFRESH } from '@/utils/constants';
@ -11,11 +12,11 @@ function HomePage() {
useLayoutEffect(() => { useLayoutEffect(() => {
if (!user) { if (!user) {
setTimeout(() => { setTimeout(() => {
router.push('/manuals'); router.push(urls.manuals);
}, TIMEOUT_UI_REFRESH); }, TIMEOUT_UI_REFRESH);
} else { } else {
setTimeout(() => { setTimeout(() => {
router.push('/library'); router.push(urls.library);
}, TIMEOUT_UI_REFRESH); }, TIMEOUT_UI_REFRESH);
} }
}, [router, user]); }, [router, user]);

View File

@ -2,6 +2,7 @@
import { useCallback, useLayoutEffect, useState } from 'react'; import { useCallback, useLayoutEffect, useState } from 'react';
import { urls } from '@/app/urls';
import DataLoader from '@/components/wrap/DataLoader'; import DataLoader from '@/components/wrap/DataLoader';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
@ -38,7 +39,7 @@ function LibraryPage() {
useLayoutEffect(() => { useLayoutEffect(() => {
if (!queryFilter || !Object.values(LibraryFilterStrategy).includes(queryFilter)) { if (!queryFilter || !Object.values(LibraryFilterStrategy).includes(queryFilter)) {
router.replace(`/library?filter=${strategy}`); router.replace(urls.library_filter(strategy));
return; return;
} }
setQuery(''); setQuery('');

View File

@ -3,6 +3,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { urls } from '@/app/urls';
import SearchBar from '@/components/ui/SearchBar'; import SearchBar from '@/components/ui/SearchBar';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { ILibraryFilter } from '@/models/miscellaneous'; import { ILibraryFilter } from '@/models/miscellaneous';
@ -37,7 +38,7 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setFilter }:
const handleChangeStrategy = useCallback( const handleChangeStrategy = useCallback(
(value: LibraryFilterStrategy) => { (value: LibraryFilterStrategy) => {
if (value !== strategy) { if (value !== strategy) {
router.push(`/library?filter=${value}`); router.push(urls.library_filter(value));
} }
}, },
[strategy, router] [strategy, router]

View File

@ -4,6 +4,7 @@ import clsx from 'clsx';
import { useLayoutEffect, useMemo, useState } from 'react'; import { useLayoutEffect, useMemo, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { urls } from '@/app/urls';
import BadgeHelp from '@/components/man/BadgeHelp'; import BadgeHelp from '@/components/man/BadgeHelp';
import DataTable, { createColumnHelper, VisibilityState } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper, VisibilityState } from '@/components/ui/DataTable';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
@ -33,7 +34,7 @@ function ViewLibrary({ items, resetQuery }: ViewLibraryProps) {
const { getUserLabel } = useUsers(); const { getUserLabel } = useUsers();
const [itemsPerPage, setItemsPerPage] = useLocalStorage<number>(storage.libraryPagination, 50); const [itemsPerPage, setItemsPerPage] = useLocalStorage<number>(storage.libraryPagination, 50);
const handleOpenItem = (item: ILibraryItem) => router.push(`/rsforms/${item.id}`); const handleOpenItem = (item: ILibraryItem) => router.push(urls.schema(item.id));
const windowSize = useWindowSize(); const windowSize = useWindowSize();

View File

@ -4,6 +4,7 @@ import axios from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { urls } from '@/app/urls';
import InfoError, { ErrorData } from '@/components/info/InfoError'; import InfoError, { ErrorData } from '@/components/info/InfoError';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
@ -53,7 +54,7 @@ function LoginPage() {
if (router.canBack()) { if (router.canBack()) {
router.back(); router.back();
} else { } else {
router.push('/library'); router.push(urls.library);
} }
}); });
} }

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import { urls } from '@/app/urls';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { useConceptOptions } from '@/context/OptionsContext'; import { useConceptOptions } from '@/context/OptionsContext';
import useQueryStrings from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
@ -16,7 +17,7 @@ function ManualsPage() {
const { mainHeight } = useConceptOptions(); const { mainHeight } = useConceptOptions();
function onSelectTopic(newTopic: HelpTopic) { function onSelectTopic(newTopic: HelpTopic) {
router.push(`/manuals?topic=${newTopic}`); router.push(urls.help_topic(newTopic));
} }
return ( return (

View File

@ -4,6 +4,7 @@ import axios from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { urls } from '@/app/urls';
import InfoError, { ErrorData } from '@/components/info/InfoError'; import InfoError, { ErrorData } from '@/components/info/InfoError';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
@ -51,8 +52,8 @@ function PasswordChangePage() {
token: token! token: token!
}; };
resetPassword(data, () => { resetPassword(data, () => {
router.replace('/'); router.replace(urls.home);
router.push('/login'); router.push(urls.login);
}); });
} }
} }

View File

@ -141,32 +141,33 @@ function FormConstituenta({
className={clsx('cc-column', 'mt-1 w-full md:w-[47.8rem] shrink-0', 'px-4 py-1')} className={clsx('cc-column', 'mt-1 w-full md:w-[47.8rem] shrink-0', 'px-4 py-1')}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<RefsInput
key='cst_term'
id='cst_term'
label='Термин'
placeholder='Обозначение, используемое в текстовых определениях'
items={schema?.items}
value={term}
initialValue={state?.term_raw ?? ''}
resolved={state?.term_resolved ?? ''}
disabled={disabled}
onChange={newValue => setTerm(newValue)}
/>
<TextArea
id='cst_typification'
dense
noBorder
disabled={true}
label='Типизация'
rows={typification.length > ROW_SIZE_IN_CHARACTERS ? 2 : 1}
value={typification}
colors='clr-app'
style={{
resize: 'none'
}}
/>
<AnimatePresence> <AnimatePresence>
<RefsInput <AnimateFade key='cst_expression_fade' hideContent={!!state && !state?.definition_formal && isElementary}>
id='cst_term'
label='Термин'
placeholder='Обозначение, используемое в текстовых определениях'
items={schema?.items}
value={term}
initialValue={state?.term_raw ?? ''}
resolved={state?.term_resolved ?? ''}
disabled={disabled}
onChange={newValue => setTerm(newValue)}
/>
<TextArea
id='cst_typification'
dense
noBorder
disabled={true}
label='Типизация'
rows={typification.length > ROW_SIZE_IN_CHARACTERS ? 2 : 1}
value={typification}
colors='clr-app'
style={{
resize: 'none'
}}
/>
<AnimateFade hideContent={!!state && !state?.definition_formal && isElementary}>
<EditorRSExpression <EditorRSExpression
id='cst_expression' id='cst_expression'
label={ label={
@ -191,7 +192,7 @@ function FormConstituenta({
setTypification={setTypification} setTypification={setTypification}
/> />
</AnimateFade> </AnimateFade>
<AnimateFade hideContent={!!state && !state?.definition_raw && isElementary}> <AnimateFade key='cst_definition_fade' hideContent={!!state && !state?.definition_raw && isElementary}>
<RefsInput <RefsInput
id='cst_definition' id='cst_definition'
label='Текстовое определение' label='Текстовое определение'
@ -205,7 +206,7 @@ function FormConstituenta({
onChange={newValue => setTextDefinition(newValue)} onChange={newValue => setTextDefinition(newValue)}
/> />
</AnimateFade> </AnimateFade>
<AnimateFade hideContent={!showConvention}> <AnimateFade key='cst_convention_fade' hideContent={!showConvention}>
<TextArea <TextArea
id='cst_convention' id='cst_convention'
spellCheck spellCheck
@ -220,6 +221,8 @@ function FormConstituenta({
</AnimateFade> </AnimateFade>
{!showConvention && (!disabled || processing) ? ( {!showConvention && (!disabled || processing) ? (
<button <button
key='cst_disable_comment'
id='cst_disable_comment'
type='button' type='button'
className='self-start cc-label clr-text-url hover:underline' className='self-start cc-label clr-text-url hover:underline'
onClick={() => setForceComment(true)} onClick={() => setForceComment(true)}
@ -229,6 +232,8 @@ function FormConstituenta({
) : null} ) : null}
{!disabled || processing ? ( {!disabled || processing ? (
<SubmitButton <SubmitButton
key='cst_form_submit'
id='cst_form_submit'
text='Сохранить изменения' text='Сохранить изменения'
className='self-center' className='self-center'
disabled={disabled || !isModified} disabled={disabled || !isModified}

View File

@ -154,77 +154,75 @@ function EditorRSExpression({
} }
return ( return (
<> <div>
<AnimatePresence> <AnimatePresence>
{showAST ? ( {showAST ? (
<DlgShowAST expression={expression} syntaxTree={syntaxTree} hideWindow={() => setShowAST(false)} /> <DlgShowAST expression={expression} syntaxTree={syntaxTree} hideWindow={() => setShowAST(false)} />
) : null} ) : null}
</AnimatePresence> </AnimatePresence>
<div> <Overlay position='top-[-0.5rem] right-0 flex'>
<Overlay position='top-[-0.5rem] right-0 flex'> <MiniButton
<MiniButton title='Изменить шрифт'
title='Изменить шрифт' onClick={toggleFont}
onClick={toggleFont} icon={<BiFontFamily size='1.25rem' className={mathFont === 'math' ? 'icon-primary' : ''} />}
icon={<BiFontFamily size='1.25rem' className={mathFont === 'math' ? 'icon-primary' : ''} />} />
/> {!disabled || model.processing ? (
{!disabled || model.processing ? (
<MiniButton
noHover
title='Отображение специальной клавиатуры'
onClick={() => setShowControls(prev => !prev)}
icon={<FaRegKeyboard size='1.25rem' className={showControls ? 'icon-primary' : ''} />}
/>
) : null}
<MiniButton <MiniButton
noHover noHover
title='Отображение списка конституент' title='Отображение специальной клавиатуры'
onClick={onToggleList} onClick={() => setShowControls(prev => !prev)}
icon={<BiListUl size='1.25rem' className={showList ? 'icon-primary' : ''} />} icon={<FaRegKeyboard size='1.25rem' className={showControls ? 'icon-primary' : ''} />}
/> />
<MiniButton ) : null}
noHover <MiniButton
title='Дерево разбора выражения' noHover
onClick={handleShowAST} title='Отображение списка конституент'
icon={<RiNodeTree size='1.25rem' className='icon-primary' />} onClick={onToggleList}
/> icon={<BiListUl size='1.25rem' className={showList ? 'icon-primary' : ''} />}
</Overlay>
<Overlay position='top-[-0.5rem] pl-[8rem] sm:pl-[4rem] right-1/2 translate-x-1/2 flex'>
<StatusBar
processing={parser.loading}
isModified={isModified}
constituenta={activeCst}
parseData={parser.parseData}
onAnalyze={() => handleCheckExpression()}
/>
<BadgeHelp topic={HelpTopic.CST_EDITOR} offset={4} />
</Overlay>
<RSInput
ref={rsInput}
value={value}
minHeight='3.8rem'
disabled={disabled}
onChange={handleChange}
onAnalyze={handleCheckExpression}
{...restProps}
/> />
<MiniButton
<RSEditorControls noHover
isOpen={showControls && (!disabled || model.processing)} title='Дерево разбора выражения'
disabled={disabled} onClick={handleShowAST}
onEdit={handleEdit} icon={<RiNodeTree size='1.25rem' className='icon-primary' />}
/> />
</Overlay>
<ParsingResult <Overlay position='top-[-0.5rem] pl-[8rem] sm:pl-[4rem] right-1/2 translate-x-1/2 flex'>
isOpen={!!parser.parseData && parser.parseData.errors.length > 0} <StatusBar
data={parser.parseData} processing={parser.loading}
disabled={disabled} isModified={isModified}
onShowError={onShowError} constituenta={activeCst}
parseData={parser.parseData}
onAnalyze={() => handleCheckExpression()}
/> />
</div> <BadgeHelp topic={HelpTopic.CST_EDITOR} offset={4} />
</> </Overlay>
<RSInput
ref={rsInput}
value={value}
minHeight='3.8rem'
disabled={disabled}
onChange={handleChange}
onAnalyze={handleCheckExpression}
{...restProps}
/>
<RSEditorControls
isOpen={showControls && (!disabled || model.processing)}
disabled={disabled}
onEdit={handleEdit}
/>
<ParsingResult
isOpen={!!parser.parseData && parser.parseData.errors.length > 0}
data={parser.parseData}
disabled={disabled}
onShowError={onShowError}
/>
</div>
); );
} }

View File

@ -56,8 +56,10 @@ function StatusBar({ isModified, processing, constituenta, parseData, onAnalyze
{processing ? <Loader key='status-loader' size={3} /> : null} {processing ? <Loader key='status-loader' size={3} /> : null}
{!processing ? ( {!processing ? (
<> <>
<StatusIcon status={status} /> <StatusIcon key='status-icon' status={status} />
<span className='pb-[0.125rem] font-controls pr-2'>{labelExpressionStatus(status)}</span> <span key='status-text' className='pb-[0.125rem] font-controls pr-2'>
{labelExpressionStatus(status)}
</span>
</> </>
) : null} ) : null}
</AnimatePresence> </AnimatePresence>

View File

@ -6,6 +6,7 @@ import fileDownload from 'js-file-download';
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import InfoError, { ErrorData } from '@/components/info/InfoError'; import InfoError, { ErrorData } from '@/components/info/InfoError';
import Divider from '@/components/ui/Divider'; import Divider from '@/components/ui/Divider';
import Loader from '@/components/ui/Loader'; import Loader from '@/components/ui/Loader';
@ -152,13 +153,7 @@ export const RSEditState = ({
); );
const viewVersion = useCallback( const viewVersion = useCallback(
(version?: number) => { (version?: number) => router.push(urls.schema(model.schemaID, version)),
if (version) {
router.push(`/rsforms/${model.schemaID}?v=${version}`);
} else {
router.push(`/rsforms/${model.schemaID}`);
}
},
[router, model] [router, model]
); );

View File

@ -5,12 +5,13 @@ import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import TabLabel from '@/components/ui/TabLabel'; import TabLabel from '@/components/ui/TabLabel';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext'; import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
import { useRSForm } from '@/context/RSFormContext';
import { useConceptOptions } from '@/context/OptionsContext'; import { useConceptOptions } from '@/context/OptionsContext';
import { useRSForm } from '@/context/RSFormContext';
import useQueryStrings from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
import { ConstituentaID, IConstituenta, IConstituentaMeta } from '@/models/rsform'; import { ConstituentaID, IConstituenta, IConstituentaMeta } from '@/models/rsform';
import { prefixes, TIMEOUT_UI_REFRESH } from '@/utils/constants'; import { prefixes, TIMEOUT_UI_REFRESH } from '@/utils/constants';
@ -34,7 +35,7 @@ function RSTabs() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const query = useQueryStrings(); const query = useQueryStrings();
const activeTab = (Number(query.get('tab')) ?? RSTabID.CARD) as RSTabID; const activeTab = (Number(query.get('tab')) ?? RSTabID.CARD) as RSTabID;
const version = Number(query.get('v')) ?? undefined; const version = query.get('v') ? Number(query.get('v')) : undefined;
const cstQuery = query.get('active'); const cstQuery = query.get('active');
const { setNoFooter, calculateHeight } = useConceptOptions(); const { setNoFooter, calculateHeight } = useConceptOptions();
@ -84,18 +85,23 @@ function RSTabs() {
if (!schema) { if (!schema) {
return; return;
} }
const versionStr = version ? `v=${version}&` : ''; const url = urls.schema_props({
id: schema.id,
tab: tab,
active: activeID,
version: version
});
if (activeID) { if (activeID) {
if (tab === activeTab && tab !== RSTabID.CST_EDIT) { if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
router.replace(`/rsforms/${schema.id}?${versionStr}tab=${tab}&active=${activeID}`); router.replace(url);
} else { } else {
router.push(`/rsforms/${schema.id}?${versionStr}tab=${tab}&active=${activeID}`); router.push(url);
} }
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) { } else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
activeID = schema.items[0].id; activeID = schema.items[0].id;
router.replace(`/rsforms/${schema.id}?${versionStr}tab=${tab}&active=${activeID}`); router.replace(url);
} else { } else {
router.push(`/rsforms/${schema.id}?${versionStr}tab=${tab}`); router.push(url);
} }
}, },
[router, schema, activeTab, version] [router, schema, activeTab, version]
@ -150,7 +156,7 @@ function RSTabs() {
} }
destroyItem(schema.id, () => { destroyItem(schema.id, () => {
toast.success('Схема удалена'); toast.success('Схема удалена');
router.push('/library'); router.push(urls.library);
}); });
}, [schema, destroyItem, router]); }, [schema, destroyItem, router]);

View File

@ -24,6 +24,7 @@ import {
} from 'react-icons/lu'; } from 'react-icons/lu';
import { VscLibrary } from 'react-icons/vsc'; import { VscLibrary } from 'react-icons/vsc';
import { urls } from '@/app/urls';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
@ -114,11 +115,11 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
} }
function handleCreateNew() { function handleCreateNew() {
router.push('/library/create'); router.push(urls.create_schema);
} }
function handleLogin() { function handleLogin() {
router.push('/login'); router.push(urls.login);
} }
return ( return (
@ -189,7 +190,7 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
<DropdownButton <DropdownButton
text='Библиотека' text='Библиотека'
icon={<VscLibrary size='1rem' className='icon-primary' />} icon={<VscLibrary size='1rem' className='icon-primary' />}
onClick={() => router.push('/library')} onClick={() => router.push(urls.library)}
/> />
</Dropdown> </Dropdown>
</div> </div>

View File

@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
import { BiInfoCircle } from 'react-icons/bi'; import { BiInfoCircle } from 'react-icons/bi';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import InfoError from '@/components/info/InfoError'; import InfoError from '@/components/info/InfoError';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Checkbox from '@/components/ui/Checkbox'; import Checkbox from '@/components/ui/Checkbox';
@ -42,7 +43,7 @@ function RegisterPage() {
if (router.canBack()) { if (router.canBack()) {
router.back(); router.back();
} else { } else {
router.push('/library'); router.push(urls.library);
} }
} }
@ -58,7 +59,7 @@ function RegisterPage() {
last_name: lastName last_name: lastName
}; };
signup(data, createdUser => { signup(data, createdUser => {
router.push(`/login?username=${createdUser.username}`); router.push(urls.login_hint(createdUser.username));
toast.success(`Пользователь успешно создан: ${createdUser.username}`); toast.success(`Пользователь успешно создан: ${createdUser.username}`);
}); });
} }

View File

@ -5,6 +5,7 @@ import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import InfoError, { ErrorData } from '@/components/info/InfoError'; import InfoError, { ErrorData } from '@/components/info/InfoError';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
@ -53,7 +54,7 @@ function EditorPassword() {
}; };
updatePassword(data, () => { updatePassword(data, () => {
toast.success('Изменения сохранены'); toast.success('Изменения сохранены');
router.push('/login'); router.push(urls.login);
}); });
} }

View File

@ -4,6 +4,7 @@ import { motion } from 'framer-motion';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { urls } from '@/app/urls';
import DataTable, { createColumnHelper } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper } from '@/components/ui/DataTable';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { ILibraryItem } from '@/models/library'; import { ILibraryItem } from '@/models/library';
@ -19,7 +20,7 @@ function ViewSubscriptions({ items }: ViewSubscriptionsProps) {
const router = useConceptNavigation(); const router = useConceptNavigation();
const intl = useIntl(); const intl = useIntl();
const openRSForm = (item: ILibraryItem) => router.push(`/rsforms/${item.id}`); const openRSForm = (item: ILibraryItem) => router.push(urls.schema(item.id));
const columns = useMemo( const columns = useMemo(
() => [ () => [

View File

@ -63,7 +63,7 @@ export const youtube = {
/** /**
* External URLs. * External URLs.
*/ */
export const urls = { export const external_urls = {
concept: 'https://www.acconcept.ru/', concept: 'https://www.acconcept.ru/',
exteor32: 'https://drive.google.com/open?id=1IHlMMwaYlAUBRSxU1RU_hXM5mFU9-oyK&usp=drive_fs', exteor32: 'https://drive.google.com/open?id=1IHlMMwaYlAUBRSxU1RU_hXM5mFU9-oyK&usp=drive_fs',
exteor64: 'https://drive.google.com/open?id=1IJt25ZRQ-ZMA6t7hOqmo5cv05WJCQKMv&usp=drive_fs', exteor64: 'https://drive.google.com/open?id=1IJt25ZRQ-ZMA6t7hOqmo5cv05WJCQKMv&usp=drive_fs',