mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
Refactoring: centralize internal URLs + animation fixes
This commit is contained in:
parent
aff116abbc
commit
9fe73a607a
|
@ -1,7 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { useConceptOptions } from '@/context/OptionsContext';
|
||||
import { urls } from '@/utils/constants';
|
||||
import { external_urls } from '@/utils/constants';
|
||||
|
||||
import TextURL from '../components/ui/TextURL';
|
||||
|
||||
|
@ -22,7 +22,7 @@ function Footer() {
|
|||
<div className='flex gap-3'>
|
||||
<TextURL text='Библиотека' href='/library' color='clr-footer' />
|
||||
<TextURL text='Справка' href='/manuals' color='clr-footer' />
|
||||
<TextURL text='Центр Концепт' href={urls.concept} color='clr-footer' />
|
||||
<TextURL text='Центр Концепт' href={external_urls.concept} color='clr-footer' />
|
||||
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='clr-footer' />
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { useConceptNavigation } from '@/context/NavigationContext';
|
|||
import { useConceptOptions } from '@/context/OptionsContext';
|
||||
import { animateNavigation } from '@/styling/animations';
|
||||
|
||||
import { urls } from '../urls';
|
||||
import Logo from './Logo';
|
||||
import NavigationButton from './NavigationButton';
|
||||
import ToggleNavigationButton from './ToggleNavigationButton';
|
||||
|
@ -17,10 +18,10 @@ function Navigation() {
|
|||
const router = useConceptNavigation();
|
||||
const { noNavigationAnimation } = useConceptOptions();
|
||||
|
||||
const navigateHome = () => router.push('/');
|
||||
const navigateLibrary = () => router.push('/library');
|
||||
const navigateHelp = () => router.push('/manuals');
|
||||
const navigateCreateNew = () => router.push('/library/create');
|
||||
const navigateHome = () => router.push(urls.home);
|
||||
const navigateLibrary = () => router.push(urls.library);
|
||||
const navigateHelp = () => router.push(urls.manuals);
|
||||
const navigateCreateNew = () => router.push(urls.create_schema);
|
||||
|
||||
return (
|
||||
<nav
|
||||
|
|
|
@ -6,6 +6,8 @@ import { useAuth } from '@/context/AuthContext';
|
|||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { useConceptOptions } from '@/context/OptionsContext';
|
||||
|
||||
import { urls } from '../urls';
|
||||
|
||||
interface UserDropdownProps {
|
||||
isOpen: boolean;
|
||||
hideDropdown: () => void;
|
||||
|
@ -18,12 +20,12 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
|||
|
||||
function navigateProfile() {
|
||||
hideDropdown();
|
||||
router.push('/profile');
|
||||
router.push(urls.profile);
|
||||
}
|
||||
|
||||
function logoutAndRedirect() {
|
||||
hideDropdown();
|
||||
logout(() => router.push('/login/'));
|
||||
logout(() => router.push(urls.login));
|
||||
}
|
||||
|
||||
function handleToggleDarkMode() {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useAuth } from '@/context/AuthContext';
|
|||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import useDropdown from '@/hooks/useDropdown';
|
||||
|
||||
import { urls } from '../urls';
|
||||
import NavigationButton from './NavigationButton';
|
||||
import UserDropdown from './UserDropdown';
|
||||
|
||||
|
@ -13,7 +14,7 @@ function UserMenu() {
|
|||
const { user } = useAuth();
|
||||
const menu = useDropdown();
|
||||
|
||||
const navigateLogin = () => router.push('/login');
|
||||
const navigateLogin = () => router.push(urls.login);
|
||||
return (
|
||||
<div ref={menu.ref} className='h-full'>
|
||||
{!user ? (
|
||||
|
|
|
@ -13,6 +13,7 @@ import RSFormPage from '@/pages/RSFormPage';
|
|||
import UserProfilePage from '@/pages/UserProfilePage';
|
||||
|
||||
import ApplicationLayout from './ApplicationLayout';
|
||||
import { routes } from './urls';
|
||||
|
||||
export const Router = createBrowserRouter([
|
||||
{
|
||||
|
@ -25,40 +26,40 @@ export const Router = createBrowserRouter([
|
|||
element: <HomePage />
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
path: routes.login,
|
||||
element: <LoginPage />
|
||||
},
|
||||
{
|
||||
path: 'signup',
|
||||
path: routes.signup,
|
||||
element: <RegisterPage />
|
||||
},
|
||||
{
|
||||
path: 'restore-password',
|
||||
element: <RestorePasswordPage />
|
||||
},
|
||||
{
|
||||
path: 'password-change',
|
||||
element: <PasswordChangePage />
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
path: routes.profile,
|
||||
element: <UserProfilePage />
|
||||
},
|
||||
{
|
||||
path: 'manuals',
|
||||
element: <ManualsPage />
|
||||
path: routes.restore_password,
|
||||
element: <RestorePasswordPage />
|
||||
},
|
||||
{
|
||||
path: 'library',
|
||||
path: routes.password_change,
|
||||
element: <PasswordChangePage />
|
||||
},
|
||||
{
|
||||
path: routes.library,
|
||||
element: <LibraryPage />
|
||||
},
|
||||
{
|
||||
path: 'library/create',
|
||||
path: routes.create_schema,
|
||||
element: <CreateRSFormPage />
|
||||
},
|
||||
{
|
||||
path: 'rsforms/:id',
|
||||
path: `${routes.rsforms}/:id`,
|
||||
element: <RSFormPage />
|
||||
},
|
||||
{
|
||||
path: routes.manuals,
|
||||
element: <ManualsPage />
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
49
rsconcept/frontend/src/app/urls.ts
Normal file
49
rsconcept/frontend/src/app/urls.ts
Normal 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}`;
|
||||
}
|
||||
};
|
|
@ -10,8 +10,8 @@ import { AnimatePresence } from 'framer-motion';
|
|||
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import Label from '@/components/ui/Label';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { useConceptOptions } from '@/context/OptionsContext';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import DlgEditReference from '@/dialogs/DlgEditReference';
|
||||
import { ReferenceType } from '@/models/language';
|
||||
import { IConstituenta } from '@/models/rsform';
|
||||
|
@ -163,7 +163,7 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
|||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={clsx('flex flex-col gap-2', cursor)}>
|
||||
<AnimatePresence>
|
||||
{showEditor ? (
|
||||
<DlgEditReference
|
||||
|
@ -180,27 +180,24 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
|||
/>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className={clsx('flex flex-col gap-2', cursor)}>
|
||||
<Label text={label} />
|
||||
<CodeMirror
|
||||
id={id}
|
||||
ref={thisRef}
|
||||
basicSetup={editorSetup}
|
||||
theme={customTheme}
|
||||
extensions={editorExtensions}
|
||||
value={isFocused ? value : value !== initialValue || showEditor ? value : resolved}
|
||||
indentWithTab={false}
|
||||
onChange={handleChange}
|
||||
editable={!disabled}
|
||||
onKeyDown={handleInput}
|
||||
onFocus={handleFocusIn}
|
||||
onBlur={handleFocusOut}
|
||||
// spellCheck= // TODO: figure out while automatic spellcheck doesn't work or implement with extension
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<Label text={label} />
|
||||
<CodeMirror
|
||||
id={id}
|
||||
ref={thisRef}
|
||||
basicSetup={editorSetup}
|
||||
theme={customTheme}
|
||||
extensions={editorExtensions}
|
||||
value={isFocused ? value : value !== initialValue || showEditor ? value : resolved}
|
||||
indentWithTab={false}
|
||||
onChange={handleChange}
|
||||
editable={!disabled}
|
||||
onKeyDown={handleInput}
|
||||
onFocus={handleFocusIn}
|
||||
onBlur={handleFocusOut}
|
||||
// spellCheck= // TODO: figure out while automatic spellcheck doesn't work or implement with extension
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import axios, { type AxiosError } from 'axios';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { urls } from '@/utils/constants';
|
||||
import { external_urls } from '@/utils/constants';
|
||||
import { isResponseHtml } from '@/utils/utils';
|
||||
|
||||
import PrettyJson from '../ui/PrettyJSON';
|
||||
|
@ -72,7 +72,7 @@ function InfoError({ error }: InfoErrorProps) {
|
|||
>
|
||||
<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 />
|
||||
Для продолжения работы перезагрузите страницу
|
||||
</p>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import TextURL from '@/components/ui/TextURL';
|
||||
import { urls } from '@/utils/constants';
|
||||
import { external_urls } from '@/utils/constants';
|
||||
|
||||
function HelpAPI() {
|
||||
return (
|
||||
|
@ -8,10 +8,10 @@ function HelpAPI() {
|
|||
<p>В качестве программного интерфейса сервера используется REST API, реализованный с помощью Django.</p>
|
||||
<p>На данный момент API находится в разработке, поэтому поддержка внешних запросов не производится.</p>
|
||||
<p>
|
||||
С описанием интерфейса можно ознакомиться <TextURL text='по ссылке' href={urls.restAPI} />.
|
||||
С описанием интерфейса можно ознакомиться <TextURL text='по ссылке' href={external_urls.restAPI} />.
|
||||
</p>
|
||||
<p>
|
||||
<TextURL text='Принять участие в разработке' href={urls.git_repo} />
|
||||
<TextURL text='Принять участие в разработке' href={external_urls.git_repo} />
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import TextURL from '@/components/ui/TextURL';
|
||||
import { urls } from '@/utils/constants';
|
||||
import { external_urls } from '@/utils/constants';
|
||||
|
||||
function HelpExteor() {
|
||||
// prettier-ignore
|
||||
|
@ -9,7 +9,7 @@ function HelpExteor() {
|
|||
<p>Экстеор 4.9 — редактор текстов систем понятий эксплицированных в родах структур</p>
|
||||
<p>Портал превосходит Экстеор в части редактирования экспликаций, но функции синтеза и вычисления интерпретации пока доступны только в Экстеоре. Также следует использовать Экстеор для выгрузки экспликаций в Word для последующей печати</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>
|
||||
<li>Работа с РС-формой системы понятий</li>
|
||||
<li>Автоматическое определение типизации выражений</li>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import TextURL from '@/components/ui/TextURL';
|
||||
import { urls } from '@/utils/constants';
|
||||
import { external_urls } from '@/utils/constants';
|
||||
|
||||
function HelpMain() {
|
||||
// prettier-ignore
|
||||
|
@ -16,8 +16,8 @@ function HelpMain() {
|
|||
<p>В меню пользователя (правый угол) доступно редактирование пользователя и изменение цветовой темы</p>
|
||||
|
||||
<h2>Поддержка</h2>
|
||||
<p>Портал разрабатывается <TextURL text='Центром Концепт' href={urls.concept}/> и является проектом с открытым исходным кодом, доступным на <TextURL text='Github' href={urls.git_repo}/></p>
|
||||
<p>Ваши пожелания по доработке, найденные ошибки и иные предложения можно направлять по email: <TextURL href={urls.mail_portal} text='portal@acconcept.ru'/></p>
|
||||
<p>Портал разрабатывается <TextURL text='Центром Концепт' href={external_urls.concept}/> и является проектом с открытым исходным кодом, доступным на <TextURL text='Github' href={external_urls.git_repo}/></p>
|
||||
<p>Ваши пожелания по доработке, найденные ошибки и иные предложения можно направлять по email: <TextURL href={external_urls.mail_portal} text='portal@acconcept.ru'/></p>
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
|||
|
||||
import EmbedYoutube from '@/components/ui/EmbedYoutube';
|
||||
import useWindowSize from '@/hooks/useWindowSize';
|
||||
import { urls, youtube } from '@/utils/constants';
|
||||
import { external_urls, youtube } from '@/utils/constants';
|
||||
|
||||
const OPT_VIDEO_H = 1080;
|
||||
|
||||
|
@ -25,9 +25,9 @@ function HelpRSLang() {
|
|||
<p>Данный математический аппарат основан на аксиоматической теории множеств Цермелло-Френкеля и аппарате родов структур Н.Бурбаки.</p>
|
||||
<p>Для ознакомления с основами родов структур можно использовать следующие материалы:</p>
|
||||
<ul>
|
||||
<li>1. <a className='underline' href={urls.intro_video}>Видео: Краткое введение в мат. аппарат</a></li>
|
||||
<li>2. <a className='underline' href={urls.ponomarev}>Текст: Учебник И. Н. Пономарева</a></li>
|
||||
<li>3. <a className='underline' href={urls.full_course}>Видео: лекции для 4 курса (второй семестр 2022-23 год)</a></li>
|
||||
<li>1. <a className='underline' href={external_urls.intro_video}>Видео: Краткое введение в мат. аппарат</a></li>
|
||||
<li>2. <a className='underline' href={external_urls.ponomarev}>Текст: Учебник И. Н. Пономарева</a></li>
|
||||
<li>3. <a className='underline' href={external_urls.full_course}>Видео: лекции для 4 курса (второй семестр 2022-23 год)</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className='justify-center w-full'>
|
||||
|
|
|
@ -17,13 +17,13 @@ interface DataLoaderProps extends CProps.AnimatedDiv {
|
|||
|
||||
function DataLoader({ id, isLoading, hasNoData, error, children, ...restProps }: DataLoaderProps) {
|
||||
return (
|
||||
<AnimatePresence mode='wait'>
|
||||
<AnimatePresence>
|
||||
{isLoading ? <Loader key={`${id}-loader`} /> : null}
|
||||
{error ? <InfoError key={`${id}-error`} error={error} /> : null}
|
||||
<AnimateFade id={id} key={`${id}-data`} removeContent={isLoading || !!error || hasNoData} {...restProps}>
|
||||
{children}
|
||||
</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>
|
||||
</AnimatePresence>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
|
||||
|
@ -8,7 +9,7 @@ function ExpectedAnonymous() {
|
|||
const router = useConceptNavigation();
|
||||
|
||||
function logoutAndRedirect() {
|
||||
logout(() => router.push('/login/'));
|
||||
logout(() => router.push(urls.login));
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -4,6 +4,7 @@ import clsx from 'clsx';
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import Checkbox from '@/components/ui/Checkbox';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
|
@ -51,7 +52,7 @@ function DlgCloneLibraryItem({ hideWindow, base }: DlgCloneLibraryItemProps) {
|
|||
};
|
||||
cloneItem(base.id, data, newSchema => {
|
||||
toast.success(`Копия создана: ${newSchema.alias}`);
|
||||
router.push(`/rsforms/${newSchema.id}`);
|
||||
router.push(urls.schema(newSchema.id));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
|||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<div className='flex items-center self-center'>
|
||||
<div key='dlg_cst_alias_picker' className='flex items-center self-center'>
|
||||
<SelectSingle
|
||||
id='dlg_cst_type'
|
||||
placeholder='Выберите тип'
|
||||
|
@ -61,6 +61,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
|||
/>
|
||||
</div>
|
||||
<TextArea
|
||||
key='dlg_cst_term'
|
||||
id='dlg_cst_term'
|
||||
spellCheck
|
||||
label='Термин'
|
||||
|
@ -69,7 +70,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
|||
value={state.term_raw}
|
||||
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
|
||||
id='dlg_cst_expression'
|
||||
label={
|
||||
|
@ -88,7 +89,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
|||
onChange={value => partialUpdate({ definition_formal: value })}
|
||||
/>
|
||||
</AnimateFade>
|
||||
<AnimateFade hideContent={!state.definition_raw && isElementary}>
|
||||
<AnimateFade key='dlg_cst_definition' hideContent={!state.definition_raw && isElementary}>
|
||||
<TextArea
|
||||
id='dlg_cst_definition'
|
||||
spellCheck
|
||||
|
@ -101,6 +102,8 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
|||
</AnimateFade>
|
||||
{!showConvention ? (
|
||||
<button
|
||||
key='dlg_cst_show_comment'
|
||||
id='dlg_cst_show_comment'
|
||||
type='button'
|
||||
className='self-start cc-label clr-text-url hover:underline'
|
||||
onClick={() => setForceComment(true)}
|
||||
|
@ -110,6 +113,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
|||
) : null}
|
||||
<AnimateFade hideContent={!showConvention}>
|
||||
<TextArea
|
||||
key='dlg_cst_convention'
|
||||
id='dlg_cst_convention'
|
||||
spellCheck
|
||||
label={isBasic ? 'Конвенция' : 'Комментарий'}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from 'react';
|
|||
import { BiDownload } from 'react-icons/bi';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import InfoError from '@/components/info/InfoError';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Checkbox from '@/components/ui/Checkbox';
|
||||
|
@ -43,7 +44,7 @@ function CreateRSFormPage() {
|
|||
if (router.canBack()) {
|
||||
router.back();
|
||||
} else {
|
||||
router.push('/library');
|
||||
router.push(urls.library);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,7 +65,7 @@ function CreateRSFormPage() {
|
|||
};
|
||||
createItem(data, newSchema => {
|
||||
toast.success('Схема успешно создана');
|
||||
router.push(`/rsforms/${newSchema.id}`);
|
||||
router.push(urls.schema(newSchema.id));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useLayoutEffect } from 'react';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { TIMEOUT_UI_REFRESH } from '@/utils/constants';
|
||||
|
@ -11,11 +12,11 @@ function HomePage() {
|
|||
useLayoutEffect(() => {
|
||||
if (!user) {
|
||||
setTimeout(() => {
|
||||
router.push('/manuals');
|
||||
router.push(urls.manuals);
|
||||
}, TIMEOUT_UI_REFRESH);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
router.push('/library');
|
||||
router.push(urls.library);
|
||||
}, TIMEOUT_UI_REFRESH);
|
||||
}
|
||||
}, [router, user]);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import DataLoader from '@/components/wrap/DataLoader';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
|
@ -38,7 +39,7 @@ function LibraryPage() {
|
|||
|
||||
useLayoutEffect(() => {
|
||||
if (!queryFilter || !Object.values(LibraryFilterStrategy).includes(queryFilter)) {
|
||||
router.replace(`/library?filter=${strategy}`);
|
||||
router.replace(urls.library_filter(strategy));
|
||||
return;
|
||||
}
|
||||
setQuery('');
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import clsx from 'clsx';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import SearchBar from '@/components/ui/SearchBar';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||
|
@ -37,7 +38,7 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setFilter }:
|
|||
const handleChangeStrategy = useCallback(
|
||||
(value: LibraryFilterStrategy) => {
|
||||
if (value !== strategy) {
|
||||
router.push(`/library?filter=${value}`);
|
||||
router.push(urls.library_filter(value));
|
||||
}
|
||||
},
|
||||
[strategy, router]
|
||||
|
|
|
@ -4,6 +4,7 @@ import clsx from 'clsx';
|
|||
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import BadgeHelp from '@/components/man/BadgeHelp';
|
||||
import DataTable, { createColumnHelper, VisibilityState } from '@/components/ui/DataTable';
|
||||
import FlexColumn from '@/components/ui/FlexColumn';
|
||||
|
@ -33,7 +34,7 @@ function ViewLibrary({ items, resetQuery }: ViewLibraryProps) {
|
|||
const { getUserLabel } = useUsers();
|
||||
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();
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import axios from 'axios';
|
|||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||
import SubmitButton from '@/components/ui/SubmitButton';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
|
@ -53,7 +54,7 @@ function LoginPage() {
|
|||
if (router.canBack()) {
|
||||
router.back();
|
||||
} else {
|
||||
router.push('/library');
|
||||
router.push(urls.library);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { useConceptOptions } from '@/context/OptionsContext';
|
||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||
|
@ -16,7 +17,7 @@ function ManualsPage() {
|
|||
const { mainHeight } = useConceptOptions();
|
||||
|
||||
function onSelectTopic(newTopic: HelpTopic) {
|
||||
router.push(`/manuals?topic=${newTopic}`);
|
||||
router.push(urls.help_topic(newTopic));
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -4,6 +4,7 @@ import axios from 'axios';
|
|||
import clsx from 'clsx';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||
import SubmitButton from '@/components/ui/SubmitButton';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
|
@ -51,8 +52,8 @@ function PasswordChangePage() {
|
|||
token: token!
|
||||
};
|
||||
resetPassword(data, () => {
|
||||
router.replace('/');
|
||||
router.push('/login');
|
||||
router.replace(urls.home);
|
||||
router.push(urls.login);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -141,32 +141,33 @@ function FormConstituenta({
|
|||
className={clsx('cc-column', 'mt-1 w-full md:w-[47.8rem] shrink-0', 'px-4 py-1')}
|
||||
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>
|
||||
<RefsInput
|
||||
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}>
|
||||
<AnimateFade key='cst_expression_fade' hideContent={!!state && !state?.definition_formal && isElementary}>
|
||||
<EditorRSExpression
|
||||
id='cst_expression'
|
||||
label={
|
||||
|
@ -191,7 +192,7 @@ function FormConstituenta({
|
|||
setTypification={setTypification}
|
||||
/>
|
||||
</AnimateFade>
|
||||
<AnimateFade hideContent={!!state && !state?.definition_raw && isElementary}>
|
||||
<AnimateFade key='cst_definition_fade' hideContent={!!state && !state?.definition_raw && isElementary}>
|
||||
<RefsInput
|
||||
id='cst_definition'
|
||||
label='Текстовое определение'
|
||||
|
@ -205,7 +206,7 @@ function FormConstituenta({
|
|||
onChange={newValue => setTextDefinition(newValue)}
|
||||
/>
|
||||
</AnimateFade>
|
||||
<AnimateFade hideContent={!showConvention}>
|
||||
<AnimateFade key='cst_convention_fade' hideContent={!showConvention}>
|
||||
<TextArea
|
||||
id='cst_convention'
|
||||
spellCheck
|
||||
|
@ -220,6 +221,8 @@ function FormConstituenta({
|
|||
</AnimateFade>
|
||||
{!showConvention && (!disabled || processing) ? (
|
||||
<button
|
||||
key='cst_disable_comment'
|
||||
id='cst_disable_comment'
|
||||
type='button'
|
||||
className='self-start cc-label clr-text-url hover:underline'
|
||||
onClick={() => setForceComment(true)}
|
||||
|
@ -229,6 +232,8 @@ function FormConstituenta({
|
|||
) : null}
|
||||
{!disabled || processing ? (
|
||||
<SubmitButton
|
||||
key='cst_form_submit'
|
||||
id='cst_form_submit'
|
||||
text='Сохранить изменения'
|
||||
className='self-center'
|
||||
disabled={disabled || !isModified}
|
||||
|
|
|
@ -154,77 +154,75 @@ function EditorRSExpression({
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<AnimatePresence>
|
||||
{showAST ? (
|
||||
<DlgShowAST expression={expression} syntaxTree={syntaxTree} hideWindow={() => setShowAST(false)} />
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
|
||||
<div>
|
||||
<Overlay position='top-[-0.5rem] right-0 flex'>
|
||||
<MiniButton
|
||||
title='Изменить шрифт'
|
||||
onClick={toggleFont}
|
||||
icon={<BiFontFamily size='1.25rem' className={mathFont === 'math' ? 'icon-primary' : ''} />}
|
||||
/>
|
||||
{!disabled || model.processing ? (
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Отображение специальной клавиатуры'
|
||||
onClick={() => setShowControls(prev => !prev)}
|
||||
icon={<FaRegKeyboard size='1.25rem' className={showControls ? 'icon-primary' : ''} />}
|
||||
/>
|
||||
) : null}
|
||||
<Overlay position='top-[-0.5rem] right-0 flex'>
|
||||
<MiniButton
|
||||
title='Изменить шрифт'
|
||||
onClick={toggleFont}
|
||||
icon={<BiFontFamily size='1.25rem' className={mathFont === 'math' ? 'icon-primary' : ''} />}
|
||||
/>
|
||||
{!disabled || model.processing ? (
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Отображение списка конституент'
|
||||
onClick={onToggleList}
|
||||
icon={<BiListUl size='1.25rem' className={showList ? 'icon-primary' : ''} />}
|
||||
title='Отображение специальной клавиатуры'
|
||||
onClick={() => setShowControls(prev => !prev)}
|
||||
icon={<FaRegKeyboard size='1.25rem' className={showControls ? 'icon-primary' : ''} />}
|
||||
/>
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Дерево разбора выражения'
|
||||
onClick={handleShowAST}
|
||||
icon={<RiNodeTree size='1.25rem' className='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}
|
||||
) : null}
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Отображение списка конституент'
|
||||
onClick={onToggleList}
|
||||
icon={<BiListUl size='1.25rem' className={showList ? 'icon-primary' : ''} />}
|
||||
/>
|
||||
|
||||
<RSEditorControls
|
||||
isOpen={showControls && (!disabled || model.processing)}
|
||||
disabled={disabled}
|
||||
onEdit={handleEdit}
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Дерево разбора выражения'
|
||||
onClick={handleShowAST}
|
||||
icon={<RiNodeTree size='1.25rem' className='icon-primary' />}
|
||||
/>
|
||||
</Overlay>
|
||||
|
||||
<ParsingResult
|
||||
isOpen={!!parser.parseData && parser.parseData.errors.length > 0}
|
||||
data={parser.parseData}
|
||||
disabled={disabled}
|
||||
onShowError={onShowError}
|
||||
<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()}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -56,8 +56,10 @@ function StatusBar({ isModified, processing, constituenta, parseData, onAnalyze
|
|||
{processing ? <Loader key='status-loader' size={3} /> : null}
|
||||
{!processing ? (
|
||||
<>
|
||||
<StatusIcon status={status} />
|
||||
<span className='pb-[0.125rem] font-controls pr-2'>{labelExpressionStatus(status)}</span>
|
||||
<StatusIcon key='status-icon' status={status} />
|
||||
<span key='status-text' className='pb-[0.125rem] font-controls pr-2'>
|
||||
{labelExpressionStatus(status)}
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
|
|
|
@ -6,6 +6,7 @@ import fileDownload from 'js-file-download';
|
|||
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||
import Divider from '@/components/ui/Divider';
|
||||
import Loader from '@/components/ui/Loader';
|
||||
|
@ -152,13 +153,7 @@ export const RSEditState = ({
|
|||
);
|
||||
|
||||
const viewVersion = useCallback(
|
||||
(version?: number) => {
|
||||
if (version) {
|
||||
router.push(`/rsforms/${model.schemaID}?v=${version}`);
|
||||
} else {
|
||||
router.push(`/rsforms/${model.schemaID}`);
|
||||
}
|
||||
},
|
||||
(version?: number) => router.push(urls.schema(model.schemaID, version)),
|
||||
[router, model]
|
||||
);
|
||||
|
||||
|
|
|
@ -5,12 +5,13 @@ import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
|||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import TabLabel from '@/components/ui/TabLabel';
|
||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { useConceptOptions } from '@/context/OptionsContext';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||
import { ConstituentaID, IConstituenta, IConstituentaMeta } from '@/models/rsform';
|
||||
import { prefixes, TIMEOUT_UI_REFRESH } from '@/utils/constants';
|
||||
|
@ -34,7 +35,7 @@ function RSTabs() {
|
|||
const router = useConceptNavigation();
|
||||
const query = useQueryStrings();
|
||||
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 { setNoFooter, calculateHeight } = useConceptOptions();
|
||||
|
@ -84,18 +85,23 @@ function RSTabs() {
|
|||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
const versionStr = version ? `v=${version}&` : '';
|
||||
const url = urls.schema_props({
|
||||
id: schema.id,
|
||||
tab: tab,
|
||||
active: activeID,
|
||||
version: version
|
||||
});
|
||||
if (activeID) {
|
||||
if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
|
||||
router.replace(`/rsforms/${schema.id}?${versionStr}tab=${tab}&active=${activeID}`);
|
||||
router.replace(url);
|
||||
} 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) {
|
||||
activeID = schema.items[0].id;
|
||||
router.replace(`/rsforms/${schema.id}?${versionStr}tab=${tab}&active=${activeID}`);
|
||||
router.replace(url);
|
||||
} else {
|
||||
router.push(`/rsforms/${schema.id}?${versionStr}tab=${tab}`);
|
||||
router.push(url);
|
||||
}
|
||||
},
|
||||
[router, schema, activeTab, version]
|
||||
|
@ -150,7 +156,7 @@ function RSTabs() {
|
|||
}
|
||||
destroyItem(schema.id, () => {
|
||||
toast.success('Схема удалена');
|
||||
router.push('/library');
|
||||
router.push(urls.library);
|
||||
});
|
||||
}, [schema, destroyItem, router]);
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
} from 'react-icons/lu';
|
||||
import { VscLibrary } from 'react-icons/vsc';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Dropdown from '@/components/ui/Dropdown';
|
||||
import DropdownButton from '@/components/ui/DropdownButton';
|
||||
|
@ -114,11 +115,11 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
|||
}
|
||||
|
||||
function handleCreateNew() {
|
||||
router.push('/library/create');
|
||||
router.push(urls.create_schema);
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
router.push('/login');
|
||||
router.push(urls.login);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -189,7 +190,7 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
|||
<DropdownButton
|
||||
text='Библиотека'
|
||||
icon={<VscLibrary size='1rem' className='icon-primary' />}
|
||||
onClick={() => router.push('/library')}
|
||||
onClick={() => router.push(urls.library)}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
|
|||
import { BiInfoCircle } from 'react-icons/bi';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import InfoError from '@/components/info/InfoError';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Checkbox from '@/components/ui/Checkbox';
|
||||
|
@ -42,7 +43,7 @@ function RegisterPage() {
|
|||
if (router.canBack()) {
|
||||
router.back();
|
||||
} else {
|
||||
router.push('/library');
|
||||
router.push(urls.library);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,7 +59,7 @@ function RegisterPage() {
|
|||
last_name: lastName
|
||||
};
|
||||
signup(data, createdUser => {
|
||||
router.push(`/login?username=${createdUser.username}`);
|
||||
router.push(urls.login_hint(createdUser.username));
|
||||
toast.success(`Пользователь успешно создан: ${createdUser.username}`);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import clsx from 'clsx';
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||
import FlexColumn from '@/components/ui/FlexColumn';
|
||||
import SubmitButton from '@/components/ui/SubmitButton';
|
||||
|
@ -53,7 +54,7 @@ function EditorPassword() {
|
|||
};
|
||||
updatePassword(data, () => {
|
||||
toast.success('Изменения сохранены');
|
||||
router.push('/login');
|
||||
router.push(urls.login);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { motion } from 'framer-motion';
|
|||
import { useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import DataTable, { createColumnHelper } from '@/components/ui/DataTable';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { ILibraryItem } from '@/models/library';
|
||||
|
@ -19,7 +20,7 @@ function ViewSubscriptions({ items }: ViewSubscriptionsProps) {
|
|||
const router = useConceptNavigation();
|
||||
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(
|
||||
() => [
|
||||
|
|
|
@ -63,7 +63,7 @@ export const youtube = {
|
|||
/**
|
||||
* External URLs.
|
||||
*/
|
||||
export const urls = {
|
||||
export const external_urls = {
|
||||
concept: 'https://www.acconcept.ru/',
|
||||
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',
|
||||
|
|
Loading…
Reference in New Issue
Block a user