mirror of
https://github.com/magnum-opus-nn-cp/frontend.git
synced 2024-11-22 01:26:43 +03:00
feat(home): add file processing loader
This commit is contained in:
parent
b7b15fd26b
commit
acaf7d0b5e
|
@ -6,7 +6,7 @@ import { PROCESS_API_URL } from './urlKeys';
|
||||||
import { QUERY_KEY_PROCESSES } from './queryKeys';
|
import { QUERY_KEY_PROCESSES } from './queryKeys';
|
||||||
|
|
||||||
export type CreateProcessDTO = Partial<Pick<TextDescriptor, 'text'>> & {
|
export type CreateProcessDTO = Partial<Pick<TextDescriptor, 'text'>> & {
|
||||||
files?: [];
|
files?: File[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateProcessResponse = {
|
export type CreateProcessResponse = {
|
||||||
|
@ -14,7 +14,33 @@ export type CreateProcessResponse = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createProcess = (data: CreateProcessDTO): Promise<CreateProcessResponse> => {
|
export const createProcess = (data: CreateProcessDTO): Promise<CreateProcessResponse> => {
|
||||||
return axios.post(`${PROCESS_API_URL}/`, data);
|
const isForm = data.files?.length !== 0;
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
let inputData: any;
|
||||||
|
if (isForm) {
|
||||||
|
inputData = new FormData();
|
||||||
|
|
||||||
|
if (data.text) {
|
||||||
|
inputData.append('text', `${data.text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.files?.forEach((file, index) => {
|
||||||
|
inputData.append(`file_${index + 1}`, file);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
inputData = {
|
||||||
|
text: data.text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios.post(`${PROCESS_API_URL}/`, inputData, {
|
||||||
|
headers: isForm
|
||||||
|
? {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
type UseCreateProcessOptions = {
|
type UseCreateProcessOptions = {
|
||||||
|
|
|
@ -5,6 +5,6 @@ export type TextDescriptor = {
|
||||||
|
|
||||||
export type ProcessDescriptor = {
|
export type ProcessDescriptor = {
|
||||||
texts: TextDescriptor[];
|
texts: TextDescriptor[];
|
||||||
done: number;
|
current: number;
|
||||||
count: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
.Attachment {
|
.Attachment {
|
||||||
@include flex-col-middle;
|
@include flex-col-middle;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Attachment__text {
|
.Attachment__text {
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
@import 'src/app/styles/vars';
|
@import 'src/app/styles/vars';
|
||||||
|
|
||||||
.Loader {
|
.Loader {
|
||||||
|
--loader-size: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 20px;
|
width: var(--loader-size);
|
||||||
height: 20px;
|
height: var(--loader-size);
|
||||||
|
color: $color-on-surface-dark-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LoaderIcon {
|
.LoaderIcon {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
color: $color-on-surface-dark-100;
|
|
||||||
animation: spinner 0.7s linear infinite;
|
animation: spinner 0.7s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,7 @@
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import s from './Loader.module.scss';
|
|
||||||
import { ReactComponent as LoaderIcon } from '../../assets/icons/loader.svg';
|
import { ReactComponent as LoaderIcon } from '../../assets/icons/loader.svg';
|
||||||
|
import s from './Loader.module.scss';
|
||||||
// export enum LoaderSize {
|
|
||||||
// /**
|
|
||||||
// * Размер лоадера 20х20
|
|
||||||
// */
|
|
||||||
// small = 'small',
|
|
||||||
// /**
|
|
||||||
// * Размер лоадера 30х30
|
|
||||||
// */
|
|
||||||
// medium = 'medium',
|
|
||||||
// /**
|
|
||||||
// * Размер лоадера 40х40
|
|
||||||
// */
|
|
||||||
// large = 'large'
|
|
||||||
// }
|
|
||||||
|
|
||||||
export interface LoaderProps {
|
export interface LoaderProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -27,7 +12,7 @@ export const Loader = memo((props: LoaderProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(s.Loader, className)}>
|
<div className={clsx(s.Loader, className)}>
|
||||||
<LoaderIcon className={clsx(s.LoaderIcon)} />
|
<LoaderIcon className={clsx(s.LoaderIcon)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,13 +2,19 @@
|
||||||
|
|
||||||
.DefaultLayout {
|
.DefaultLayout {
|
||||||
@include grid-for(desktop-small);
|
@include grid-for(desktop-small);
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
@include tablet-down {
|
@include tablet-down {
|
||||||
@include grid-for(tablet-large);
|
@include grid-for(tablet-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include mobile-down {
|
||||||
|
@include grid-for(mobile-small);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.DefaultLayout__container {
|
.DefaultLayout__container {
|
||||||
|
@include flex-col;
|
||||||
grid-column: 3 / span 8;
|
grid-column: 3 / span 8;
|
||||||
|
|
||||||
@include tablet-down {
|
@include tablet-down {
|
||||||
|
@ -17,14 +23,27 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.DefaultLayout__header {
|
.DefaultLayout__header {
|
||||||
@include flex-middle;
|
display: grid;
|
||||||
column-gap: $spacing-medium;
|
gap: $spacing-small-4x $spacing-medium;
|
||||||
height: $header-height;
|
|
||||||
margin-bottom: $spacing-medium-x;
|
margin-bottom: $spacing-medium-x;
|
||||||
|
|
||||||
|
@include mobile-up {
|
||||||
|
grid-template: 'mopus cp cbr'/ auto 1fr auto;
|
||||||
|
height: $header-height;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include mobile-down {
|
||||||
|
grid-template:
|
||||||
|
'mopus cp'
|
||||||
|
'mopus cbr'
|
||||||
|
/ 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.DefaultLayout__logo {
|
.DefaultLayout__logo {
|
||||||
width: auto;
|
width: fit-content;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,8 +52,33 @@
|
||||||
background-color: $color-accent;
|
background-color: $color-accent;
|
||||||
padding: $spacing-small-2x;
|
padding: $spacing-small-2x;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
grid-area: mopus;
|
||||||
|
height: $header-height;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DefaultLayout__logo_cp {
|
||||||
|
grid-area: cp;
|
||||||
|
|
||||||
|
@include mobile-up {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include mobile-down {
|
||||||
|
height: 24px;
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.DefaultLayout__logo_cbr {
|
.DefaultLayout__logo_cbr {
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
grid-area: cbr;
|
||||||
|
|
||||||
|
@include mobile-down {
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.DefaultLayout__content {
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
|
@ -3,7 +3,9 @@
|
||||||
$textarea-height: 192px;
|
$textarea-height: 192px;
|
||||||
|
|
||||||
.HomePage {
|
.HomePage {
|
||||||
|
@include flex-col;
|
||||||
padding-bottom: $spacing-medium-x;
|
padding-bottom: $spacing-medium-x;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.HomePage__title {
|
.HomePage__title {
|
||||||
|
@ -22,6 +24,11 @@ $textarea-height: 192px;
|
||||||
background-color: $color-background-dark-100;
|
background-color: $color-background-dark-100;
|
||||||
border-radius: $radius-large;
|
border-radius: $radius-large;
|
||||||
padding: $spacing-medium;
|
padding: $spacing-medium;
|
||||||
|
|
||||||
|
@include mobile-down {
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.HomePage__dropBox {
|
.HomePage__dropBox {
|
||||||
|
@ -54,8 +61,10 @@ $textarea-height: 192px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.HomePage__button {
|
.HomePage__submitButton {
|
||||||
margin-left: auto;
|
@include mobile-up {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.HomePage__buttonHint {
|
.HomePage__buttonHint {
|
||||||
|
@ -80,17 +89,18 @@ $textarea-height: 192px;
|
||||||
border-radius: $radius-small;
|
border-radius: $radius-small;
|
||||||
background-color: $color-background-primary;
|
background-color: $color-background-primary;
|
||||||
padding: $spacing-small-x;
|
padding: $spacing-small-x;
|
||||||
|
|
||||||
|
@include mobile-down {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.HomePage__uploadContainer {
|
.HomePage__uploadContainer {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
//padding-top: $spacing-small-3x;
|
|
||||||
//border-top: 1px dashed $color-divider-darker;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.HomePage__uploadButton {
|
.HomePage__uploadButton {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
//margin-bottom: $spacing-small-4x;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.HomePage__uploadHint {
|
.HomePage__uploadHint {
|
||||||
|
@ -101,6 +111,21 @@ $textarea-height: 192px;
|
||||||
.HomePage__orHint {
|
.HomePage__orHint {
|
||||||
margin-bottom: $spacing-small-3x;
|
margin-bottom: $spacing-small-3x;
|
||||||
letter-spacing: 0.06rem;
|
letter-spacing: 0.06rem;
|
||||||
//color: $color-text-tertiary;
|
}
|
||||||
//margin-left: 70px;
|
|
||||||
|
.HomePage__loaderContainer {
|
||||||
|
@include flex-col-middle;
|
||||||
|
justify-content: center;
|
||||||
|
row-gap: $spacing-small-4x;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.HomePage__loader {
|
||||||
|
--loader-size: 96px;
|
||||||
|
color: $color-background-secondary !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.HomePage__loaderText {
|
||||||
|
@include text-programming-code-regular;
|
||||||
|
font-size: $font-size-16;
|
||||||
}
|
}
|
|
@ -13,6 +13,7 @@ import { useProcess } from '../../api/process/getProcess';
|
||||||
import { useSingleTimeout } from '../../hooks/useSingleTimeout';
|
import { useSingleTimeout } from '../../hooks/useSingleTimeout';
|
||||||
import { Upload } from '../../components/Upload';
|
import { Upload } from '../../components/Upload';
|
||||||
import { Attachment } from '../../components/Attachment';
|
import { Attachment } from '../../components/Attachment';
|
||||||
|
import { Loader } from '../../components/Loader';
|
||||||
import { ReactComponent as PlusIcon } from './assets/plus.svg';
|
import { ReactComponent as PlusIcon } from './assets/plus.svg';
|
||||||
import s from './HomePage.module.scss';
|
import s from './HomePage.module.scss';
|
||||||
|
|
||||||
|
@ -38,21 +39,26 @@ export const HomePage: ReactFCC = () => {
|
||||||
|
|
||||||
const [processId, setProcessId] = useState<string | null>(null);
|
const [processId, setProcessId] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data: process, refetch: refetchProcess } = useProcess({
|
const {
|
||||||
|
data: process,
|
||||||
|
refetch: refetchProcess,
|
||||||
|
isFetching: processFetching
|
||||||
|
} = useProcess({
|
||||||
processId: processId || '',
|
processId: processId || '',
|
||||||
config: {
|
config: {
|
||||||
enabled: !!processId
|
enabled: !!processId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createProcess, isLoading } = useCreateProcess();
|
const { mutateAsync: createProcess, isLoading: createProcessLoading } = useCreateProcess();
|
||||||
|
|
||||||
const timeout = useSingleTimeout();
|
const timeout = useSingleTimeout();
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<FormFields> = useCallback(
|
const onSubmit: SubmitHandler<FormFields> = useCallback(
|
||||||
async (data) => {
|
async (data) => {
|
||||||
const response = await createProcess({
|
const response = await createProcess({
|
||||||
text: data.text
|
text: data.text,
|
||||||
|
files: data.files
|
||||||
});
|
});
|
||||||
|
|
||||||
setProcessId(response.id);
|
setProcessId(response.id);
|
||||||
|
@ -65,7 +71,7 @@ export const HomePage: ReactFCC = () => {
|
||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
timeout.set(async () => {
|
timeout.set(async () => {
|
||||||
const { data: process } = await refetchProcess();
|
const { data: process } = await refetchProcess();
|
||||||
if (process && process.done < process.count) {
|
if (process && process.current < process.total) {
|
||||||
startPolling();
|
startPolling();
|
||||||
}
|
}
|
||||||
}, PROCESS_POLLING_MS);
|
}, PROCESS_POLLING_MS);
|
||||||
|
@ -77,13 +83,6 @@ export const HomePage: ReactFCC = () => {
|
||||||
}
|
}
|
||||||
}, [processId, refetchProcess, timeout]);
|
}, [processId, refetchProcess, timeout]);
|
||||||
|
|
||||||
// todo it's mock!
|
|
||||||
useEffect(() => {
|
|
||||||
if (process && process.done === process.count) {
|
|
||||||
alert(`Кредитный рейтинг ${process.texts[0].score}`);
|
|
||||||
}
|
|
||||||
}, [process]);
|
|
||||||
|
|
||||||
// ------ Обработка DnD ------
|
// ------ Обработка DnD ------
|
||||||
|
|
||||||
const currentText = watch('text');
|
const currentText = watch('text');
|
||||||
|
@ -110,88 +109,88 @@ export const HomePage: ReactFCC = () => {
|
||||||
|
|
||||||
// ------ Логика UI ------
|
// ------ Логика UI ------
|
||||||
|
|
||||||
|
const isLoading = createProcessLoading || processFetching || !!(process && process.current < process.total);
|
||||||
const isDisabled = !currentText && currentFiles.length === 0;
|
const isDisabled = !currentText && currentFiles.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={s.HomePage}>
|
<div className={s.HomePage}>
|
||||||
<Heading size={HeadingSize.H2} className={s.HomePage__title}>
|
{!isLoading ? (
|
||||||
Анализ текстовых пресс-релизов
|
<div className={s.HomePage__main}>
|
||||||
</Heading>
|
<Heading size={HeadingSize.H2} className={s.HomePage__title}>
|
||||||
|
Анализ текстовых пресс-релизов
|
||||||
|
</Heading>
|
||||||
|
|
||||||
<Text className={s.HomePage__text} variant={ETextVariants.BODY_M_REGULAR}>
|
<Text className={s.HomePage__text} variant={ETextVariants.BODY_M_REGULAR}>
|
||||||
Позволяет оценить кредитный рейтинг компании на основе пресс-релиза с выделением в тексте меток по различным
|
Позволяет оценить кредитный рейтинг компании на основе пресс-релиза с выделением в тексте меток по различным
|
||||||
метрикам.
|
метрикам.
|
||||||
</Text>
|
|
||||||
|
|
||||||
<form className={s.HomePage__box} onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div
|
|
||||||
className={clsx(s.HomePage__dropBox, {
|
|
||||||
[s.HomePage__dropBox_hidden]: isDragActive
|
|
||||||
})}
|
|
||||||
{...getRootProps()}>
|
|
||||||
{currentFiles.length === 0 ? (
|
|
||||||
<Textarea
|
|
||||||
className={s.HomePage__textarea}
|
|
||||||
registration={register('text')}
|
|
||||||
rows={8}
|
|
||||||
placeholder={'Текст пресс-релиза...'}
|
|
||||||
error={!!errors.text}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className={s.HomePage__filesContainer}>
|
|
||||||
{currentFiles.map((item, index) => (
|
|
||||||
<Attachment
|
|
||||||
file={item}
|
|
||||||
onClick={() => setValue('files', [...currentFiles.filter((i) => i !== item)])}
|
|
||||||
key={index}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Text className={s.HomePage__uploadHint} variant={ETextVariants.CAPTION_S_REGULAR}>
|
|
||||||
Загрузите файлы, перетащив их мышкой или нажав кнопку ниже <br />
|
|
||||||
Доступны файлы Word, Excel, PDF, TXT, изображения
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Upload {...getInputProps()}>
|
<form className={s.HomePage__box} onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Button
|
<div
|
||||||
component={'div'}
|
className={clsx(s.HomePage__dropBox, {
|
||||||
className={s.HomePage__uploadButton}
|
[s.HomePage__dropBox_hidden]: isDragActive
|
||||||
variant={ButtonVariant.secondary}
|
})}
|
||||||
size={ButtonSize.small_x}>
|
{...getRootProps()}>
|
||||||
Загрузить файлы
|
{currentFiles.length === 0 ? (
|
||||||
</Button>
|
<Textarea
|
||||||
</Upload>
|
className={s.HomePage__textarea}
|
||||||
|
registration={register('text')}
|
||||||
|
rows={8}
|
||||||
|
placeholder={'Текст пресс-релиза...'}
|
||||||
|
error={!!errors.text}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={s.HomePage__filesContainer}>
|
||||||
|
{currentFiles.map((item, index) => (
|
||||||
|
<Attachment
|
||||||
|
file={item}
|
||||||
|
onClick={() => setValue('files', [...currentFiles.filter((i) => i !== item)])}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={s.HomePage__dropBoxPlaceholder}>
|
<Text className={s.HomePage__uploadHint} variant={ETextVariants.CAPTION_S_REGULAR}>
|
||||||
<PlusIcon className={s.HomePage__dropBoxPlaceholderIcon} />
|
Загрузите файлы, перетащив их мышкой или нажав кнопку ниже <br />
|
||||||
|
Доступны файлы Word, Excel, PDF, TXT, изображения
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Upload {...getInputProps()}>
|
||||||
|
<Button
|
||||||
|
component={'div'}
|
||||||
|
className={s.HomePage__uploadButton}
|
||||||
|
variant={ButtonVariant.secondary}
|
||||||
|
size={ButtonSize.small_x}>
|
||||||
|
Загрузить файлы
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
|
||||||
|
<div className={s.HomePage__dropBoxPlaceholder}>
|
||||||
|
<PlusIcon className={s.HomePage__dropBoxPlaceholderIcon} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider className={s.HomePage__divider} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type={'submit'}
|
||||||
|
className={s.HomePage__submitButton}
|
||||||
|
size={ButtonSize.large_x}
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={isDisabled}>
|
||||||
|
Отправить
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={s.HomePage__loaderContainer}>
|
||||||
|
<Loader className={s.HomePage__loader} />
|
||||||
|
<div className={s.HomePage__loaderText}>
|
||||||
|
{process?.current ?? 0}/{process?.total ?? 0}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{/*<Text className={s.HomePage__orHint} variant={ETextVariants.PROGRAMMING_CODE_MEDIUM}>*/}
|
|
||||||
{/* ИЛИ*/}
|
|
||||||
{/*</Text>*/}
|
|
||||||
|
|
||||||
{/*<Button className={s.HomePage__button} size={ButtonSize.large}>*/}
|
|
||||||
{/* Выбрать DOCX/PDF файл*/}
|
|
||||||
{/*</Button>*/}
|
|
||||||
|
|
||||||
{/*<Text className={s.HomePage__buttonHint} variant={ETextVariants.BODY_S_REGULAR}>*/}
|
|
||||||
{/* Или перетащите файл мышкой*/}
|
|
||||||
{/*</Text>*/}
|
|
||||||
|
|
||||||
<Divider className={s.HomePage__divider} />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type={'submit'}
|
|
||||||
className={s.HomePage__button}
|
|
||||||
size={ButtonSize.large_x}
|
|
||||||
isLoading={isLoading}
|
|
||||||
disabled={isDisabled}>
|
|
||||||
Отправить
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user