refactor: clean up

This commit is contained in:
Pavel Torbeev 2023-09-10 07:31:49 +03:00
parent 52c93b18b5
commit efffd63ae1
56 changed files with 9 additions and 2004 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
PORT=3001

View File

@ -1,35 +0,0 @@
import {axios} from '../../lib/axios';
import {CONVERT_API_URL} from './urlKeys';
import {MutationConfig} from '../../lib/react-query';
import {useMutation} from '@tanstack/react-query';
export type ConvertDTO = {
pdf: File;
};
export type ConvertResponse = {
pdf: string;
pptx: string;
};
export const convert = (data: ConvertDTO): Promise<ConvertResponse> => {
const inputData = new FormData();
inputData.append('pdf', data.pdf);
return axios.post(CONVERT_API_URL, inputData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
};
type UseConvertOptions = {
config?: MutationConfig<typeof convert>;
};
export const useConvert = ({ config }: UseConvertOptions = {}) => {
return useMutation({
...config,
mutationFn: convert
});
};

View File

@ -1,65 +0,0 @@
import { useMutation } from '@tanstack/react-query';
import { MutationConfig, queryClient } from '../../lib/react-query';
import { axios } from '../../lib/axios';
import { Answer, PitchDeck } from './types';
import { DECKS_API_URL, QUESTION_API_URL, QUESTION_PARAM_DECK_ID, QUESTION_PARAM_QUESTION_ID } from './urlKeys';
import { QUERY_KEY_ANSWER } from './queryKeys';
export type CreateAnswerDTO = {
deckId: number;
questionId: number;
answer: any;
isFile?: boolean;
file?: File;
};
export type CreateAnswerResponse = Answer;
export const createAnswer = (data: CreateAnswerDTO): Promise<CreateAnswerResponse> => {
const path =
QUESTION_API_URL.replace(`:${QUESTION_PARAM_DECK_ID}`, String(data.deckId)).replace(
`:${QUESTION_PARAM_QUESTION_ID}`,
String(data.questionId)
) + '/';
let inputData: any;
if (data.isFile) {
inputData = new FormData();
if (data.answer) {
inputData.append('answer', `${data.answer}`);
}
for (const key in data) {
if (key !== 'answer' && key !== 'isFile' && key !== 'deckId' && key !== 'questionId') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
inputData.append(key, data[key]);
}
}
} else {
inputData = {
answer: data.answer
};
}
return axios.post(path, inputData, {
headers: data.isFile
? {
'Content-Type': 'multipart/form-data'
}
: {}
});
};
type UseCreateAnswerOptions = {
config?: MutationConfig<typeof createAnswer>;
};
export const useCreateAnswer = ({ config }: UseCreateAnswerOptions = {}) => {
return useMutation({
onMutate: async () => {
await queryClient.cancelQueries([QUERY_KEY_ANSWER]);
},
...config,
mutationFn: createAnswer
});
};

View File

@ -1,29 +0,0 @@
import {PitchDeck} from './types';
import {DECKS_API_URL} from './urlKeys';
import {MutationConfig, queryClient} from '../../lib/react-query';
import {useMutation} from '@tanstack/react-query';
import {QUERY_KEY_DECKS} from './queryKeys';
import { axios } from '../../lib/axios';
export type CreateDeckDTO = Pick<PitchDeck, 'description'>;
export type CreateDeckResponse = PitchDeck;
export const createDeck = (data: CreateDeckDTO): Promise<CreateDeckResponse> => {
return axios.post(DECKS_API_URL, data);
};
type UseCreateDeckOptions = {
config?: MutationConfig<typeof createDeck>;
};
export const useCreateDeck = ({ config }: UseCreateDeckOptions = {}) => {
return useMutation({
onMutate: async () => {
await queryClient.cancelQueries([QUERY_KEY_DECKS]);
},
...config,
mutationFn: createDeck
});
};

View File

@ -1,50 +0,0 @@
import {Question} from './types';
import {axios} from '../../lib/axios';
import {
DECK_API_URL,
QUESTION_API_URL,
QUESTION_PARAM_DECK_ID,
QUESTION_PARAM_QUESTION_ID,
} from './urlKeys';
import {ExtractFnReturnType, QueryConfig} from '../../lib/react-query';
import {useQuery} from '@tanstack/react-query';
import {QUERY_KEY_DECKS, QUERY_KEY_QUESTION} from './queryKeys';
export type GetDeckResponse = {
deck: {
name: string;
description: string;
},
slides: {
slide: number;
data: {
slug: string;
answer: any;
photos?: string[]
}[];
}[];
};
export const getDeck = ({ deckId }: { deckId: number }): Promise<GetDeckResponse> => {
return axios.get(
DECK_API_URL
.replace(`:${QUESTION_PARAM_DECK_ID}`, String(deckId))
);
};
type QueryFnType = typeof getDeck;
type UseDeckOptions = {
deckId: number;
config?: QueryConfig<QueryFnType>;
};
export const useDeck = ({ deckId, config }: UseDeckOptions) => {
return useQuery<ExtractFnReturnType<QueryFnType>>({
...config,
queryKey: [QUERY_KEY_DECKS, deckId],
queryFn: async () => {
return await getDeck({ deckId });
},
});
};

View File

@ -1,31 +0,0 @@
import {Question} from './types';
import {axios} from '../../lib/axios';
import {FIRST_QUESTION_API_URL, FIRST_QUESTION_PARAM} from './urlKeys';
import {ExtractFnReturnType, QueryConfig} from '../../lib/react-query';
import {useQuery} from '@tanstack/react-query';
import {QUERY_KEY_FIRST_QUESTION} from './queryKeys';
export type GetFirstQuestionResponse = Question;
export const getFirstQuestion = ({ deckId }: { deckId: number; }): Promise<GetFirstQuestionResponse> => {
return axios.get(FIRST_QUESTION_API_URL.replace(`:${FIRST_QUESTION_PARAM}`, String(deckId)));
};
type QueryFnType = typeof getFirstQuestion;
type UseFirstQuestionOptions = {
deckId: number;
config?: QueryConfig<QueryFnType>;
};
export const useFirstQuestion = ({ deckId, config }: UseFirstQuestionOptions) => {
return useQuery<ExtractFnReturnType<QueryFnType>>({
...config,
queryKey: [QUERY_KEY_FIRST_QUESTION, deckId],
queryFn: async () => {
const process = await getFirstQuestion({ deckId });
return process;
},
});
};

View File

@ -1,38 +0,0 @@
import {Question} from './types';
import {axios} from '../../lib/axios';
import {
QUESTION_API_URL,
QUESTION_PARAM_DECK_ID,
QUESTION_PARAM_QUESTION_ID,
} from './urlKeys';
import {ExtractFnReturnType, QueryConfig} from '../../lib/react-query';
import {useQuery} from '@tanstack/react-query';
import {QUERY_KEY_QUESTION} from './queryKeys';
export type GetQuestionResponse = Question;
export const getQuestion = ({ deckId, questionId }: { deckId: number; questionId: number; }): Promise<GetQuestionResponse> => {
return axios.get(
QUESTION_API_URL
.replace(`:${QUESTION_PARAM_DECK_ID}`, String(deckId))
.replace(`:${QUESTION_PARAM_QUESTION_ID}`, String(questionId))
);
};
type QueryFnType = typeof getQuestion;
type UseQuestionOptions = {
deckId: number;
questionId: number;
config?: QueryConfig<QueryFnType>;
};
export const useQuestion = ({ deckId, questionId, config }: UseQuestionOptions) => {
return useQuery<ExtractFnReturnType<QueryFnType>>({
...config,
queryKey: [QUERY_KEY_QUESTION, deckId, questionId],
queryFn: async () => {
return await getQuestion({ deckId, questionId });
},
});
};

View File

@ -1,6 +0,0 @@
export * from './types';
export * from './urlKeys';
export * from './queryKeys';
export * from './createDeck';
export * from './getFirstQuestion';

View File

@ -1,4 +0,0 @@
export const QUERY_KEY_DECKS = 'decks';
export const QUERY_KEY_FIRST_QUESTION = 'firstQuestion';
export const QUERY_KEY_QUESTION = 'question';
export const QUERY_KEY_ANSWER = 'answer';

View File

@ -1,45 +0,0 @@
export enum EntityType {
text = 'text', // +
number = 'number', // +
range = 'range', // +
multiple_range = 'multiple_range', // +
select = 'select', // +
link = 'link', // + добавить валидацию
date = 'date', // +
// photo = 'photo',
multiple_photo = 'multiple_photo',
photo_description = 'photo_description',
multiple_link_description = 'multiple_link_description', //
multiple_photo_description = 'multiple_photo_description',
// multiple_links = 'multiple_links', // не пригодилось
multiple_date_description = 'multiple_date_description', //
text_array = 'text_array', // используется только в подсказке
cards = 'cards', // используется только в подсказке
}
export type PitchDeck = {
id: number;
name: string;
description?: string;
questions?: any[];
};
export type Hint = {
type: EntityType;
value: any;
}
export type Question = {
id: number;
text: string;
type: EntityType;
hint: Hint | false;
next_id: number;
params: { [key: string]: any } | null;
};
export type Answer = {
answer: any;
deck: number;
question: number;
}

View File

@ -1,13 +0,0 @@
export const DECKS_API_URL = '/decks/';
export const FIRST_QUESTION_PARAM = 'deckId';
export const FIRST_QUESTION_API_URL = `/decks/question/:${FIRST_QUESTION_PARAM}`;
export const QUESTION_PARAM_DECK_ID = 'deckId';
export const QUESTION_PARAM_QUESTION_ID = 'questionId';
export const QUESTION_API_URL = `/decks/question/:${FIRST_QUESTION_PARAM}/:${QUESTION_PARAM_QUESTION_ID}`;
export const DECK_PARAM = 'deckId';
export const DECK_API_URL = `/decks/question/:${FIRST_QUESTION_PARAM}/presentation`;
export const CONVERT_API_URL = '/decks/pdf-to-pptx';

View File

@ -1,16 +1,14 @@
import { Route, Routes } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import { ChatPage } from '../../pages/chat';
import { HomePage } from '../../pages/home'; import { HomePage } from '../../pages/home';
import { DefaultLayout } from '../../pages/_layouts/DefaultLayout'; import { DefaultLayout } from '../../pages/_layouts/DefaultLayout';
import { TextPage } from '../../pages/text'; import { TextPage } from '../../pages/text';
import { ResponsePage } from '../../pages/response'; import { ResponsePage } from '../../pages/response';
import { CHAT_PAGE_ROUTE, HOME_PAGE_ROUTE, RESPONSE_PAGE_ROUTE, TEXT_PAGE_ROUTE } from './routes'; import { HOME_PAGE_ROUTE, RESPONSE_PAGE_ROUTE, TEXT_PAGE_ROUTE } from './routes';
export const AppRoutes = () => { export const AppRoutes = () => {
return ( return (
<Routes> <Routes>
<Route element={<DefaultLayout />}> <Route element={<DefaultLayout />}>
<Route path={CHAT_PAGE_ROUTE} element={<ChatPage />} />
<Route path={HOME_PAGE_ROUTE} element={<HomePage />} /> <Route path={HOME_PAGE_ROUTE} element={<HomePage />} />
<Route path={RESPONSE_PAGE_ROUTE} element={<ResponsePage />} /> <Route path={RESPONSE_PAGE_ROUTE} element={<ResponsePage />} />
<Route path={TEXT_PAGE_ROUTE} element={<TextPage />} /> <Route path={TEXT_PAGE_ROUTE} element={<TextPage />} />

View File

@ -1,4 +1,3 @@
export const CHAT_PAGE_ROUTE = `/chat`;
export const HOME_PAGE_ROUTE = `/`; export const HOME_PAGE_ROUTE = `/`;
export const RESPONSE_PAGE_PARAM = 'processId'; export const RESPONSE_PAGE_PARAM = 'processId';

View File

@ -1,8 +1,5 @@
// export const BACKEND_URL = 'http://192.168.83.181:8000'; // export const BACKEND_URL = 'http://192.168.107.4';
// export const BACKEND_URL = 'http://192.168.22.4:8000'; export const BACKEND_URL = 'https://4810-176-59-122-187.ngrok-free.app';
// export const BACKEND_URL = 'https://ed68-77-234-219-9.ngrok-free.app';
// export const BACKEND_URL = 'https://16c2-77-234-219-9.ngrok-free.app';
export const BACKEND_URL = 'http://192.168.107.4';
export const BACKEND_MEDIA_PORT = '8000'; export const BACKEND_MEDIA_PORT = '8000';
export const API_URL = BACKEND_URL + '/api'; export const API_URL = BACKEND_URL + '/api';

View File

@ -1,90 +0,0 @@
@import 'src/app/styles/vars';
// Общий лейаут
.ChatPage {
}
.ChatPage__inner {
position: relative;
//width: $breakpoint-tablet-small;
width: 100%;
height: 100vh;
max-height: 100svh;
margin: 0 auto;
overflow: hidden;
background-image: url('./assets/background2.jpg');
background-size: 150%;
@include media-down(tablet-small) {
width: 100%;
}
}
.ChatPage__container {
height: 100%;
position: absolute;
width: 100%;
z-index: 1;
//background-color: rgba($color-on-surface-dark-100, 0.6);
//background-color: $color-surface-primary;
background-color: rgba($color-surface-primary, 0.7);
backdrop-filter: blur(10px);
padding: $spacing-medium-x $spacing-small 0;
overflow-y: scroll;
@include scrollbar;
@include flex-col;
}
// Инпут
.ChatPage__inputContainer {
position: absolute;
width: 100%;
min-height: 100px;
padding: $spacing-small;
bottom: 0;
background-color: rgba($color-surface-primary, 0.6);
backdrop-filter: blur(10px);
z-index: 2;
@include flex-col;
max-height: 100svh;
overflow-y: auto;
@include scrollbar;
}
.ChatPage__skipButton {
width: 100% !important;
margin-top: $spacing-small;
}
// Секция с контентом и сообщениями
.ChatPage__content {
flex: 1;
display: flex;
flex-direction: column;
//justify-content: flex-end;
row-gap: $spacing-small;
}
.ChatPage__message {
width: fit-content;
max-width: 66%;
@include media-down(tablet-small) {
max-width: 100%;
}
}
.ChatPage__message_right {
margin-left: auto;
}
.ChatPage__message_left {
margin-right: auto;
}

View File

@ -1,202 +0,0 @@
import {useCallback, useEffect, useRef, useState} from 'react';
import clsx from 'clsx';
import s from './ChatPage.module.scss';
import {ReactFCC} from '../../utils/ReactFCC';
import {ChatContent} from './components/ChatContent';
import {ChatItemType, useChatHistory} from './store/history';
import {SubmitHandler} from 'react-hook-form';
import {getFirstQuestion, useCreateDeck} from '../../api/deck';
import {ChatFormInitial} from './components/ChatForm/ChatFormInittial';
import {useChatUi} from './hooks/useChatUi';
import {useQuestion} from '../../api/deck/getQuestion';
import {useCreateAnswer} from '../../api/deck/createAnswer';
import {QuestionFactory} from './components/ChatForm/QuestionFactory';
import {useSingleTimeout} from '../../hooks/useSingleTimeout';
import {usePrevious} from '../../hooks/usePrevious';
import {generateAnswerFromData} from './utils/generateAnswerFromData';
import {generateTextFromAnswer} from './utils/generateTextFromAnswer';
import {Button, ButtonVariant} from '../../components/Button';
import {useNavigate} from 'react-router-dom';
import {generateFieldsForFileAnswers} from './utils/generateFieldsForFileAnswers';
export interface ChatPageProps {
/**
* Дополнительный css-класс
*/
className?: string;
}
const QUESTION_POLLING_MS = 1000;
const DEFAULT_DECK_ID = 0;
const DEFAULT_QUESTION_ID = 0;
export const ChatPage: ReactFCC<ChatPageProps> = (props) => {
const {className} = props;
const timeout = useSingleTimeout();
// Работа с UI
const { backgroundRef, containerRef, contentRef, inputContainerRef } = useChatUi();
const { history, pushHistory } = useChatHistory();
const initRef = useRef(false);
// Устанавливаем первый вопрос в чат
useEffect(() => {
if (!initRef.current) {
pushHistory({
type: ChatItemType.receive,
text: 'Введите описание проекта (не больше 2-3 предложений)',
});
initRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
/**
* Работа с API
*/
const [deckId, setDeckId] = useState(DEFAULT_DECK_ID);
const [questionId, setQuestionId] = useState(DEFAULT_QUESTION_ID);
const { mutateAsync: createDeck } = useCreateDeck();
const onSubmitInitial: SubmitHandler<any> = useCallback(async (data) => {
const deck = await createDeck({
description: data.description
});
setDeckId(deck.id);
pushHistory({
type: ChatItemType.send,
text: deck.description as string
});
const firstQuestion = await getFirstQuestion({ deckId });
setQuestionId(firstQuestion.id);
}, [createDeck, deckId, pushHistory]);
// Начинаем пинг-понг вопросов-ответов
const { data: question, refetch: refetchQuestion } = useQuestion({
deckId,
questionId,
config: {
enabled: !!(deckId && questionId),
// keepPreviousData: true,
}
});
const prevQuestion = usePrevious(question);
useEffect(() => {
if (question && question.id !== prevQuestion?.id) {
timeout.clear();
pushHistory({
type: ChatItemType.receive,
text: question.text
});
const startPolling = () => {
timeout.set(async () => {
const { data: newQuestion } = await refetchQuestion();
if (newQuestion?.hint && !newQuestion.hint.type) {
startPolling();
}
}, QUESTION_POLLING_MS);
}
if (question?.hint && !question.hint.type) {
startPolling();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pushHistory, question]);
const { mutateAsync: createAnswer } = useCreateAnswer();
const navigate = useNavigate();
const onSubmit: SubmitHandler<any> = useCallback(async (data) => {
if (!question || !data.value) {
return;
}
timeout.clear();
const answerValue = generateAnswerFromData(question, data);
const additionalFields = generateFieldsForFileAnswers(question, data);
const answer = await createAnswer({
deckId,
questionId,
answer: answerValue,
...additionalFields,
isFile: !!additionalFields && Object.keys(additionalFields).length !== 0
});
pushHistory({
type: ChatItemType.send,
text: generateTextFromAnswer(question.type, answer, additionalFields)
});
if (question.next_id) {
setQuestionId(question!.next_id);
} else {
// navigate(DECK_PAGE_ROUTE.replace(`:${DECK_PAGE_PARAM}`, String(deckId)))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [createAnswer, deckId, pushHistory, question, questionId]);
// Пропуск вопроса
const onSkip = useCallback(() => {
if (question && !question.params?.required) {
if (question.next_id) {
setQuestionId(question!.next_id);
} else {
// navigate(DECK_PAGE_ROUTE.replace(`:${DECK_PAGE_PARAM}`, String(deckId)))
}
}
}, [deckId, navigate, question]);
// ---------- Скролл чата ----------
// todo при печатании текста тоже двигать скролл
useEffect(() => {
if (contentRef.current && inputContainerRef.current) {
contentRef.current.style.paddingBottom = inputContainerRef.current.scrollHeight + 'px';
containerRef.current?.scrollTo({ top: contentRef.current.scrollHeight });
}
}, [containerRef, contentRef, history, question, inputContainerRef]);
return (
<div className={clsx(s.ChatPage, className)}>
<div className={s.ChatPage__inner} ref={backgroundRef}>
<div className={s.ChatPage__container} ref={containerRef}>
<div className={s.ChatPage__content} ref={contentRef}>
<ChatContent history={history} />
</div>
</div>
<div className={s.ChatPage__inputContainer} ref={inputContainerRef}>
{question ? (
<>
<QuestionFactory onSubmit={onSubmit} {...question} />
{!question.params?.required && (
<Button
className={s.ChatPage__skipButton}
variant={ButtonVariant.secondary}
onClick={() => onSkip()}
>Пропустить</Button>
)}
</>
) : !deckId ? (
<ChatFormInitial onSubmit={onSubmitInitial} />
) : null}
</div>
</div>
</div>
);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

View File

@ -1,86 +0,0 @@
import clsx from 'clsx';
import {ReactFCC} from '../../../utils/ReactFCC';
import s from '../ChatPage.module.scss';
import {Message, MessageType, MessageVariant} from '../../../components/Message';
import {Fragment, memo} from 'react';
import {mediaQuery, useMediaQueryResult} from '../../../hooks/useMediaQueryResult';
import {ChatItem, ChatItemType} from '../store/history';
export interface ChatMockProps {
/**
* Дополнительный css-класс
*/
className?: string;
history: ChatItem[];
}
export const ChatContent: ReactFCC<ChatMockProps> = memo(function ChatMock(props) {
const { history } = props;
const isLarge = useMediaQueryResult(mediaQuery.desktopMediumUp);
const messageTypeForSend = isLarge ? MessageType.left : MessageType.right;
const messageTypeClassForSend = isLarge ? s.ChatPage__message_left : s.ChatPage__message_right;
return (
<>
{history.map((item, index) => (
<Message
className={clsx(s.ChatPage__message, {
[messageTypeClassForSend]: item.type === ChatItemType.send,
[s.ChatPage__message_left]: item.type === ChatItemType.receive,
})}
type={item.type === ChatItemType.send ? messageTypeForSend : MessageType.left}
variant={item.type === ChatItemType.send ? MessageVariant.primary : MessageVariant.secondary}
key={index}>
{item.text}
</Message>
))}
{/*{Array(5).fill(null).map((_, index) => (*/}
{/* <Fragment key={index}>*/}
{/* <Message*/}
{/* className={clsx(s.ChatPage__message, s.ChatPage__message_left)}*/}
{/* type={MessageType.left}*/}
{/* variant={MessageVariant.secondary}>*/}
{/* Какие метрики вы используете чтобы отслеживать прогресс развития проекта?*/}
{/* </Message>*/}
{/* <Message*/}
{/* className={clsx(s.ChatPage__message, messageTypeClassForSend)}*/}
{/* type={messageTypeForSend}*/}
{/* variant={MessageVariant.primary}>*/}
{/* Возможными метриками для отслеживания прогресса могут быть: количество скачиваний и использования приложения/сервиса, количество активных пользователей, уровень удовлетворенности пользователей, объем продаж/дохода, показатели роста/расширения компании и др.*/}
{/* </Message>*/}
{/* <Message*/}
{/* className={clsx(s.ChatPage__message, s.ChatPage__message_left)}*/}
{/* type={MessageType.left}*/}
{/* variant={MessageVariant.secondary}>*/}
{/* На чем вы зарабатываете? Сколько и за что вам платят клиенты*/}
{/* </Message>*/}
{/* <Message*/}
{/* className={clsx(s.ChatPage__message, messageTypeClassForSend)}*/}
{/* type={messageTypeForSend}*/}
{/* variant={MessageVariant.primary}>*/}
{/* Проект может зарабатывать на платной подписке*/}
{/* </Message>*/}
{/* <Message*/}
{/* className={clsx(s.ChatPage__message, s.ChatPage__message_left)}*/}
{/* type={MessageType.left}*/}
{/* variant={MessageVariant.secondary}>*/}
{/* Какие метрики вы используете чтобы отслеживать прогресс развития проекта?*/}
{/* </Message>*/}
{/* <Message*/}
{/* className={clsx(s.ChatPage__message, messageTypeClassForSend)}*/}
{/* type={messageTypeForSend}*/}
{/* variant={MessageVariant.primary}>*/}
{/* Возможными метриками для отслеживания прогресса могут быть: количество скачиваний и использования приложения/сервиса, количество активных пользователей, уровень удовлетворенности пользователей, объем продаж/дохода, показатели роста/расширения компании и др.*/}
{/* </Message>*/}
{/* </Fragment>*/}
{/*))}*/}
</>
);
});

View File

@ -1,54 +0,0 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import s from './components/ChatFormText/ChatFormText.module.scss';
import {Textarea} from '../../../../components/Textarea';
import {KeyboardEvent} from 'react';
import {isKey} from '../../../../utils/isKey';
import {Key} from 'ts-key-enum';
import {Button} from '../../../../components/Button';
import {Form} from '../../../../components/Form';
import {SubmitHandler} from 'react-hook-form';
import {ReactComponent as RightIcon} from '../../../../assets/icons/right.svg';
export interface ChatFormInitialProps {
/**
* Дополнительный css-класс
*/
className?: string;
onSubmit: SubmitHandler<any>;
}
export const ChatFormInitial: ReactFCC<ChatFormInitialProps> = (props) => {
const {onSubmit} = props;
return (
<Form>
{({ register, handleSubmit }) => {
return (
<div className={s.ChatFormText__richInput}>
<Textarea
className={s.ChatFormText__input}
placeholder={'Введите сообщение'}
rows={1}
cols={33}
onKeyDown={(e: KeyboardEvent) => {
if (isKey(e.nativeEvent, Key.Enter)) {
e.preventDefault();
handleSubmit(onSubmit)(e);
}
}}
registration={register('description', {
required: true,
max: 1000,
})}
/>
<Button className={s.ChatFormText__richInputButton} onClick={handleSubmit(onSubmit)}>
<RightIcon className={s.ChatFormText__buttonIcon} />
</Button>
</div>
)
}}
</Form>
);
};

View File

@ -1,244 +0,0 @@
import {ReactFCC} from '../../../../utils/ReactFCC';
import {EntityType, Question} from '../../../../api/deck';
import {Form} from '../../../../components/Form';
import {ChatFormText} from './components/ChatFormText';
import {ChatFormSelect} from './components/ChatFormSelect';
import {ChatFormMultipleRange, RangeType} from './components/ChatFormMultipleRange';
import {ChatFormMultipleDateDescription} from './components/ChatFormMultipleDateDescription';
import {ChatFormRange} from './components/ChatFormRange';
import {ChatFormMultipleLinkDescription} from './components/ChatFormMultipleLinkDescription';
import {ChatFormPhotoDescription} from './components/ChatFormPhotoDescription';
import {ChatFormMultiplePhoto} from './components/ChatFormMultiplePhoto';
import {ChatFormMultiplePhotoDescription} from './components/ChatFormMultiplePhotoDescription';
export interface QuestionFactoryProps {
type: EntityType;
params: Question['params'];
onSubmit: (data: any) => void;
hint?: Question['hint'];
}
export const QuestionFactory: ReactFCC<QuestionFactoryProps> = (props) => {
const {type, params, onSubmit, hint} = props;
switch (type) {
case EntityType.text:
return (
<Form>
{({ handleSubmit, register, setValue }) => (
<ChatFormText
type={'textarea'}
registration={register('value', {
required: true,
maxLength: params?.max_length
})}
onSubmit={handleSubmit(onSubmit)}
setValue={(val) => setValue('value', val)}
hint={hint}
/>
)}
</Form>
)
case EntityType.number:
return (
<Form>
{({ handleSubmit, register, setValue }) => (
<ChatFormText
type={'number'}
registration={register('value', {
required: true,
maxLength: params?.max_length
})}
onSubmit={handleSubmit(onSubmit)}
setValue={(val) => setValue('value', val)}
hint={hint}
/>
)}
</Form>
)
case EntityType.date:
return (
<Form>
{({ handleSubmit, register, setValue }) => (
<ChatFormText
type={'date'}
registration={register('value', {
required: true,
maxLength: params?.max_length
})}
onSubmit={handleSubmit(onSubmit)}
setValue={(val) => setValue('value', val)}
hint={hint}
/>
)}
</Form>
)
case EntityType.link:
return (
<Form>
{({ handleSubmit, register, setValue }) => (
<ChatFormText
type={'text'}
registration={register('value', {
required: true,
maxLength: params?.max_length
})}
onSubmit={handleSubmit(onSubmit)}
setValue={(val) => setValue('value', val)}
hint={hint}
/>
)}
</Form>
)
case EntityType.select:
return (
<Form>
{({ handleSubmit, register, control }) => (
<ChatFormSelect
registration={register('value', { required: true })}
control={control}
options={params?.options || []}
onSubmit={handleSubmit(onSubmit)}
/>
)}
</Form>
);
case EntityType.multiple_range:
return (
<Form options={{
values: {
value: {
...(params?.scrollbars || []).reduce((acc: any, i: RangeType) => {
acc[i.slug] = i.min_value;
return acc;
}, {})
}
}
}}>
{({ handleSubmit, register, control, setValue }) => (
<ChatFormMultipleRange
registration={register('value', { required: true })}
control={control as any}
scrollbars={params?.scrollbars || []}
onSubmit={handleSubmit(onSubmit)}
setValue={(value) => setValue('value', value)}
hint={hint}
/>
)}
</Form>
)
case EntityType.multiple_date_description:
return (
<Form options={{
values: {
value: {
[new Date().toISOString()]: '',
}
}
}}>
{({ handleSubmit, register, control, setValue }) => (
<ChatFormMultipleDateDescription
registration={register('value', { required: true })}
control={control as any}
onSubmit={handleSubmit(onSubmit)}
setValue={(value) => setValue('value', value)}
hint={hint}
/>
)}
</Form>
)
case EntityType.range:
return (
<Form options={{
values: {
value: params?.min_value ?? 0
}
}}>
{({ handleSubmit, register, control, setValue }) => (
<ChatFormRange
registration={register('value', { required: true })}
control={control as any}
onSubmit={handleSubmit(onSubmit)}
setValue={(value) => setValue('value', value)}
hint={hint}
range={params as RangeType}
/>
)}
</Form>
)
case EntityType.multiple_link_description:
return (
<Form options={{
values: {
value: {
'Ссылка': '',
}
}
}}>
{({ handleSubmit, register, control, setValue }) => (
<ChatFormMultipleLinkDescription
registration={register('value', { required: true })}
control={control as any}
onSubmit={handleSubmit(onSubmit)}
setValue={(value) => setValue('value', value)}
hint={hint}
/>
)}
</Form>
)
case EntityType.photo_description:
return (
<Form options={{
values: {
value: {
file: null,
text: '',
}
}
}}>
{({ handleSubmit, register, control }) => (
<ChatFormPhotoDescription
registration={register('value', { required: true })}
control={control as any}
onSubmit={handleSubmit(onSubmit)}
/>
)}
</Form>
);
case EntityType.multiple_photo:
return (
<Form options={{
values: {
value: {}
}
}}>
{({ handleSubmit, register, control }) => (
<ChatFormMultiplePhoto
registration={register('value', { required: true })}
control={control as any}
onSubmit={handleSubmit(onSubmit)}
/>
)}
</Form>
);
case EntityType.multiple_photo_description:
return (
<Form options={{
values: {
value: []
}
}}>
{({ handleSubmit, register, control }) => (
<ChatFormMultiplePhotoDescription
registration={register('value', { required: true })}
control={control as any}
onSubmit={handleSubmit(onSubmit)}
/>
)}
</Form>
);
default:
return null;
}
};

View File

@ -1,38 +0,0 @@
@import 'src/app/styles/vars';
.ChatFormMultipleDateDescription {
@include flex-col;
gap: $spacing-small;
}
.ChatFormMultipleDateDescription__items {
@include flex-col;
gap: $spacing-small;
}
.ChatFormMultipleDateDescription__group {
@include flex-col;
align-items: flex-end;
gap: $spacing-small-3x;
}
.ChatFormMultipleDateDescription__input {
width: 100%;
}
.ChatFormMultipleDateDescription__textarea {
width: 100%;
}
.ChatFormMultipleDateDescription__buttons {
@include flex;
gap: $spacing-small-3x;
}
.ChatFormMultipleDateDescription__hint {
white-space: pre-wrap;
}
.ChatFormMultipleDateDescription__itemName {
align-self: flex-start;
}

View File

@ -1,97 +0,0 @@
import clsx from 'clsx';
import {ReactFCC} from '../../../../../../utils/ReactFCC';
import {Control, Controller, FieldValues, UseFormRegisterReturn} from 'react-hook-form';
import {SimpleButton} from '../../../SimpleButton';
import s from './ChatFormMultipleDateDescription.module.scss';
import {Hint} from '../../../../../../api/deck';
import {Input} from '../../../../../../components/Input';
import {Textarea} from '../../../../../../components/Textarea';
import {Button, ButtonVariant} from '../../../../../../components/Button';
import {Hint as HintCmp, HintsContainer} from '../../../../../../components/Hint';
import {formatDate} from '../../../../../../utils/fomat';
import format from 'date-fns/format';
export interface ChatFormMultipleDateDescriptionProps {
className?: string;
registration: Partial<UseFormRegisterReturn>;
control: Control<FieldValues>;
onSubmit: (e: any) => void;
setValue: (value: any) => void;
hint?: Hint | false;
}
export const ChatFormMultipleDateDescription: ReactFCC<ChatFormMultipleDateDescriptionProps> = (props) => {
const {className, registration, control, onSubmit, hint, setValue} = props;
return (
<div className={clsx(s.ChatFormMultipleDateDescription, className)}>
<Controller control={control} render={({ field: { value, onChange }}) => (
<>
<HintsContainer isLoading={hint && !hint.value}>
{hint && hint.value && (
<HintCmp
className={s.ChatFormMultipleDateDescription__hint}
onClick={() => {
const newValue: any = {};
for (const date in hint.value) {
newValue[format(new Date(date),'yyyy-MM-dd')] = hint.value[date]
}
setValue({ ...hint.value })
}}
>
{Object.entries(hint.value).map(([key, val]) => `${formatDate(key)}: ${val}`).join('\n')}
</HintCmp>
)}
</HintsContainer>
<div className={s.ChatFormMultipleDateDescription__items}>
{Object.entries(value).map(([date, text]: any, index, { length: arrLength }) => {
return (
<div className={s.ChatFormMultipleDateDescription__group} key={index}>
<Input className={s.ChatFormMultipleDateDescription__input}
type={'date'}
value={format(new Date(date),'yyyy-MM-dd')}
onChange={(e) => {
const newValue = { ...value };
const text = newValue[date];
delete newValue[date];
onChange({ ...newValue, [new Date(e.target.value).toISOString()]: text })
}}
/>
<Textarea
rows={1}
className={s.ChatFormMultipleDateDescription__textarea}
placeholder={'Текст'}
value={text}
onChange={(e) => {
onChange({ ...value, [date]: e.target.value })
}}
/>
<div className={s.ChatFormMultipleDateDescription__buttons}>
<Button variant={ButtonVariant.secondary} onClick={() => {
const newValue = { ...value };
delete newValue[date];
if (Object.keys(newValue).length !== 0) {
onChange({ ...newValue })
}
}}>Удалить</Button>
{index === arrLength - 1 && (
<Button variant={ButtonVariant.secondary}
onClick={() => {
onChange({ ...value, [new Date().toISOString()]: '' })
}}
>Добавить</Button>
)}
</div>
</div>
)
})}
</div>
<SimpleButton className={s.ChatFormMultipleRange__button} onClick={onSubmit} />
</>
)} name={registration.name!} />
</div>
)
};

View File

@ -1 +0,0 @@
export * from './ChatFormMultipleDateDescription';

View File

@ -1,93 +0,0 @@
import clsx from 'clsx';
import {ReactFCC} from '../../../../../../utils/ReactFCC';
import {Control, Controller, FieldValues, UseFormRegisterReturn} from 'react-hook-form';
import {SimpleButton} from '../../../SimpleButton';
import s from '../ChatFormMultipleDateDescription/ChatFormMultipleDateDescription.module.scss';
import {Hint} from '../../../../../../api/deck';
import {Input} from '../../../../../../components/Input';
import {Textarea} from '../../../../../../components/Textarea';
import {Button, ButtonVariant} from '../../../../../../components/Button';
import {Hint as HintCmp, HintsContainer} from '../../../../../../components/Hint';
export interface ChatFormMultipleLinkDescriptionProps {
className?: string;
registration: Partial<UseFormRegisterReturn>;
control: Control<FieldValues>;
onSubmit: (e: any) => void;
setValue: (value: any) => void;
hint?: Hint | false;
}
export const ChatFormMultipleLinkDescription: ReactFCC<ChatFormMultipleLinkDescriptionProps> = (props) => {
const {className, registration, control, onSubmit, hint, setValue} = props;
return (
<div className={clsx(s.ChatFormMultipleDateDescription, className)}>
<Controller control={control} render={({ field: { value, onChange }}) => (
<>
{/*<HintsContainer isLoading={hint && !hint.value}>*/}
{/* {hint && hint.value && (*/}
{/* <HintCmp*/}
{/* className={s.ChatFormMultipleDateDescription__hint}*/}
{/* onClick={() => setValue({ ...hint.value })}*/}
{/* >*/}
{/* {Object.entries(hint.value).map(([key, val]) => `${key}: ${val}`).join('\n')}*/}
{/* </HintCmp>*/}
{/* )}*/}
{/*</HintsContainer>*/}
<div className={s.ChatFormMultipleDateDescription__items}>
{Object.entries(value).map(([link, text]: any, index, { length: arrLength }) => {
return (
<div className={s.ChatFormMultipleDateDescription__group} key={index}>
<Input
className={s.ChatFormMultipleDateDescription__input}
type={'text'}
placeholder={'Ссылка'}
value={link}
onChange={(e) => {
const newValue = { ...value };
const text = newValue[link];
delete newValue[link];
onChange({ ...newValue, [e.target.value]: text })
}}
/>
<Textarea
rows={1}
className={s.ChatFormMultipleDateDescription__textarea}
placeholder={'Текст'}
value={text}
onChange={(e) => {
onChange({ ...value, [link]: e.target.value })
}}
/>
<div className={s.ChatFormMultipleDateDescription__buttons}>
{Object.entries(value).filter((i) => i[0] !== link).length !== 0 && (
<Button variant={ButtonVariant.secondary} onClick={() => {
const newValue = { ...value };
delete newValue[link];
if (Object.keys(newValue).length !== 0) {
onChange({ ...newValue })
}
}}>Удалить</Button>
)}
{index === arrLength - 1 && (
<Button variant={ButtonVariant.secondary}
onClick={() => {
onChange({ ...value, 'Link': '' })
}}
>Добавить</Button>
)}
</div>
</div>
)
})}
</div>
<SimpleButton className={s.ChatFormMultipleRange__button} onClick={onSubmit} />
</>
)} name={registration.name!} />
</div>
)
};

View File

@ -1 +0,0 @@
export * from './ChatFormMultipleLinkDescription';

View File

@ -1,50 +0,0 @@
import { ChangeEvent } from 'react';
import { Control, Controller, FieldValues, UseFormRegisterReturn } from 'react-hook-form';
import clsx from 'clsx';
import { ReactFCC } from '../../../../../../utils/ReactFCC';
import s from '../ChatFormPhotoDescription/ChatFormPhotoDescription.module.scss';
import { Upload } from '../../../../../../components/Upload';
import { SimpleButton } from '../../../SimpleButton';
export interface ChatFormMultiplePhotoProps {
className?: string;
onSubmit: (e: any) => void;
registration: Partial<UseFormRegisterReturn>;
control: Control<FieldValues>;
}
export const ChatFormMultiplePhoto: ReactFCC<ChatFormMultiplePhotoProps> = (props) => {
const { className, onSubmit, registration, control } = props;
return (
<div className={clsx(s.ChatFormPhotoDescription, className)}>
<Controller
control={control}
render={({ field: { value, onChange } }) => (
<>
{value && Object.values(value).map((file: any, index) => <p key={index}>Загружен файл: {file.name}</p>)}
<Upload
multiple={true}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files) {
return;
}
const files: any = { ...value };
Array.from(e.target.files).forEach((file, index) => {
files[`file_${Object.keys(files).length + 1}`] = file;
});
onChange({ ...files });
}}
/>
<SimpleButton onClick={onSubmit} />
</>
)}
name={registration.name!}
/>
</div>
);
};

View File

@ -1 +0,0 @@
export * from './ChatFormMultiplePhoto';

View File

@ -1,82 +0,0 @@
import { ChangeEvent } from 'react';
import { Control, Controller, FieldValues, UseFormRegisterReturn } from 'react-hook-form';
import clsx from 'clsx';
import { ReactFCC } from '../../../../../../utils/ReactFCC';
import { SimpleButton } from '../../../SimpleButton';
import s from '../ChatFormMultipleDateDescription/ChatFormMultipleDateDescription.module.scss';
import { Textarea } from '../../../../../../components/Textarea';
import { Button, ButtonVariant } from '../../../../../../components/Button';
import { Upload } from '../../../../../../components/Upload';
export interface ChatFormMultiplePhotoDescriptionProps {
className?: string;
registration: Partial<UseFormRegisterReturn>;
control: Control<FieldValues>;
onSubmit: (e: any) => void;
}
export const ChatFormMultiplePhotoDescription: ReactFCC<ChatFormMultiplePhotoDescriptionProps> = (props) => {
const { className, registration, control, onSubmit } = props;
return (
<div className={clsx(s.ChatFormMultipleDateDescription, className)}>
<Controller
control={control}
render={({ field: { value, onChange } }) => (
<>
<div className={s.ChatFormMultipleDateDescription__items}>
{value.map((item: any, index: number, { length: arrLength }: any) => {
return (
<div className={s.ChatFormMultipleDateDescription__group} key={index}>
{item.file && (
<p className={s.ChatFormMultipleDateDescription__itemName}>Загружен файл: {item.file.name}</p>
)}
<Textarea
rows={1}
className={s.ChatFormMultipleDateDescription__textarea}
placeholder={'Описание'}
value={item.text}
onChange={(e) => {
const itemIndex = value.indexOf(item);
const newItem = { ...item, text: e.target.value };
onChange([...value.slice(0, itemIndex), newItem, ...value.slice(itemIndex + 1)]);
}}
/>
<div className={s.ChatFormMultipleDateDescription__buttons}>
<Button
variant={ButtonVariant.secondary}
onClick={() => {
const newValue = value.filter((i: any) => i !== item);
if (Object.keys(newValue).length !== 0) {
onChange([...newValue]);
}
}}>
Удалить
</Button>
</div>
</div>
);
})}
</div>
<Upload
onChange={(e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) {
return;
}
onChange([...value, { file, text: '' }]);
e.target.value = '';
}}
/>
<SimpleButton className={s.ChatFormMultipleRange__button} onClick={onSubmit} />
</>
)}
name={registration.name!}
/>
</div>
);
};

View File

@ -1 +0,0 @@
export * from './ChatFormMultiplePhotoDescription';

View File

@ -1,20 +0,0 @@
@import 'src/app/styles/vars';
.ChatFormMultipleRange {
@include flex-col;
max-width: unset;
align-items: stretch;
width: 100%;
margin: 0 auto;
gap: $spacing-small;
}
.ChatFormMultipleRange__items {
@include flex-col;
gap: $spacing-small;
flex: 1;
}
.ChatFormMultipleRange__button {
width: 100%;
}

View File

@ -1,63 +0,0 @@
import clsx from 'clsx';
import {ReactFCC} from '../../../../../../utils/ReactFCC';
import {Control, Controller, FieldValues, UseFormRegisterReturn} from 'react-hook-form';
import {SimpleButton} from '../../../SimpleButton';
import s from './ChatFormMultipleRange.module.scss';
import {Range} from '../../../../../../components/Range';
import {Hint} from '../../../../../../api/deck';
import {Hint as HintCmp, HintsContainer} from '../../../../../../components/Hint';
import {currencyFormatter} from '../../../../../../utils/fomat';
export interface RangeType {
name: string;
slug: string;
min_value: number;
max_value: number;
}
export interface ChatFormSelectProps {
className?: string;
registration: Partial<UseFormRegisterReturn>;
control: Control<FieldValues>;
scrollbars: RangeType[];
onSubmit: (e: any) => void;
setValue: (value: any) => void;
hint?: Hint | false;
}
export const slugsForFormat = ['sam', 'som', 'tam', 'sum', 'cac', 'ltv'];
export const ChatFormMultipleRange: ReactFCC<ChatFormSelectProps> = (props) => {
const {className, registration, control, scrollbars, onSubmit, hint, setValue} = props;
return (
<div className={clsx(s.ChatFormMultipleRange, className)}>
<Controller control={control} render={({ field: { value, onChange }}) => (
<>
<HintsContainer isLoading={hint && !hint.value}>
{hint && hint.value && Object.entries(hint.value).map(([key, val], index) => {
const range = scrollbars.find((j) => j.slug === key);
return range ? (
<HintCmp onClick={() => setValue({ ...value, [key]: val })} key={index}>{`${range.name}: ${slugsForFormat.includes(range.slug) ? currencyFormatter.format(val as any) : val}`}</HintCmp>
) : null
})}
</HintsContainer>
<div className={s.ChatFormMultipleRange__items}>
{scrollbars.map((item, index) => (
<Range
label={item.name} min={item.min_value} max={item.max_value} step={((item.max_value - item.min_value) / 100) || 100}
value={value[item.slug]}
onChange={(val) => onChange({ ...value, [item.slug]: val })}
format={slugsForFormat.includes(item.slug)}
key={index} />
))}
</div>
<SimpleButton className={s.ChatFormMultipleRange__button} onClick={onSubmit} />
</>
)} name={registration.name!} />
</div>
)
};

View File

@ -1 +0,0 @@
export * from './ChatFormMultipleRange';

View File

@ -1,40 +0,0 @@
@import 'src/app/styles/vars';
.ChatFormPhotoDescription {
@include flex-col;
gap: $spacing-small;
}
//
//.ChatFormText__richInput {
// @include flex;
// position: relative;
// width: 100%;
// min-height: $chat-input-height;
// height: 100%;
// align-items: stretch;
//
// &::before {
// content: '';
// position: absolute;
// width: 100%;
// }
//}
//
//.ChatFormText__input {
// border-radius: $chat-input-radius 0 0 $chat-input-radius !important;
// flex: 1;
// min-height: 50px !important;
//}
//
//.ChatFormText__richInputButton {
// width: $chat-input-height !important;
// height: auto !important;
// border-radius: 0 $chat-input-radius $chat-input-radius 0 !important;
// padding: 0 !important;
//}
//
//.ChatFormText__buttonIcon {
// width: 24px;
// height: 24px;
//}

View File

@ -1,55 +0,0 @@
import { ChangeEvent } from 'react';
import { Control, Controller, FieldValues, UseFormRegisterReturn } from 'react-hook-form';
import clsx from 'clsx';
import { ReactFCC } from '../../../../../../utils/ReactFCC';
import { Textarea } from '../../../../../../components/Textarea';
import { Upload } from '../../../../../../components/Upload';
import { SimpleButton } from '../../../SimpleButton';
import s from './ChatFormPhotoDescription.module.scss';
export interface ChatFormPhotoDescriptionProps {
className?: string;
onSubmit: (e: any) => void;
registration: Partial<UseFormRegisterReturn>;
control: Control<FieldValues>;
}
export const ChatFormPhotoDescription: ReactFCC<ChatFormPhotoDescriptionProps> = (props) => {
const { className, onSubmit, registration, control } = props;
return (
<div className={clsx(s.ChatFormPhotoDescription, className)}>
<Controller
control={control}
render={({ field: { value, onChange } }) => (
<>
{value.file && <p>Загружен файл: {value.file.name}</p>}
<Upload
onChange={(e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) {
return;
}
onChange({ ...value, file });
e.target.value = '';
}}
/>
<Textarea
placeholder={'Текст'}
value={value.text}
onChange={(e) => {
onChange({ ...value, text: e.target.value });
}}
/>
<SimpleButton onClick={onSubmit} />
</>
)}
name={registration.name!}
/>
</div>
);
};

View File

@ -1 +0,0 @@
export * from './ChatFormPhotoDescription';

View File

@ -1,52 +0,0 @@
import clsx from 'clsx';
import {ReactFCC} from '../../../../../../utils/ReactFCC';
import {Control, Controller, FieldValues, UseFormRegisterReturn} from 'react-hook-form';
import {SimpleButton} from '../../../SimpleButton';
import s from './../ChatFormMultipleRange/ChatFormMultipleRange.module.scss';
import {Range} from '../../../../../../components/Range';
import {Hint} from '../../../../../../api/deck';
import {Hint as HintCmp, HintsContainer} from '../../../../../../components/Hint';
import {RangeType, slugsForFormat} from '../ChatFormMultipleRange';
import {currencyFormatter} from '../../../../../../utils/fomat';
export interface ChatFormRangeProps {
className?: string;
registration: Partial<UseFormRegisterReturn>;
control: Control<FieldValues>;
onSubmit: (e: any) => void;
setValue: (value: any) => void;
hint?: Hint | false;
range: RangeType;
}
export const ChatFormRange: ReactFCC<ChatFormRangeProps> = (props) => {
const {className, registration, control, onSubmit, hint, setValue, range} = props;
return (
<div className={clsx(s.ChatFormMultipleRange, className)}>
<Controller control={control} render={({ field: { value, onChange }}) => (
<>
<HintsContainer isLoading={hint && !hint.value}>
{hint && hint.value && (
<HintCmp onClick={() => setValue(hint.value)}>
{slugsForFormat.includes(range.slug) ? currencyFormatter.format(hint.value) : hint.value}
</HintCmp>
)}
</HintsContainer>
<div className={s.ChatFormMultipleRange__items}>
<Range
value={value}
min={range.min_value} max={range.max_value} step={((range.max_value - range.min_value) / 100) || 100}
onChange={(val) => onChange(val)}
format={slugsForFormat.includes(range.slug)}
/>
</div>
<SimpleButton className={s.ChatFormMultipleRange__button} onClick={onSubmit} />
</>
)} name={registration.name!} />
</div>
)
};

View File

@ -1 +0,0 @@
export * from './ChatFormRange';

View File

@ -1,12 +0,0 @@
@import 'src/app/styles/vars';
.ChatFormSelect {
@include flex-col;
align-items: flex-start;
gap: $spacing-small;
}
.ChatFormSelect__items {
@include flex-col;
gap: $spacing-small-3x;
}

View File

@ -1,38 +0,0 @@
import clsx from 'clsx';
import s from './ChatFormSelect.module.scss';
import {ReactFCC} from '../../../../../../utils/ReactFCC';
import {Control, Controller, FieldValues, UseFormRegisterReturn} from 'react-hook-form';
import {Radio} from '../../../../../../components/Radio';
import {SimpleButton} from '../../../SimpleButton';
export interface ChatFormSelectProps {
/**
* Дополнительный css-класс
*/
className?: string;
registration: Partial<UseFormRegisterReturn>;
control: Control<FieldValues>;
options: string[];
onSubmit: (e: any) => void;
}
export const ChatFormSelect: ReactFCC<ChatFormSelectProps> = (props) => {
const {className, registration, control, options, onSubmit} = props;
return (
<div className={clsx(s.ChatFormSelect, className)}>
<Controller control={control} render={({ field: { value, onChange }}) => (
<>
<div className={s.ChatFormSelect__items}>
{options.map((item, index) => (
<Radio name={registration.name} label={item} value={item} checked={item === value} onChange={(val) => onChange(val)} key={index} />
))}
</div>
<SimpleButton onClick={onSubmit} />
</>
)} name={registration.name!} />
</div>
)
};

View File

@ -1 +0,0 @@
export * from './ChatFormSelect';

View File

@ -1,42 +0,0 @@
@import 'src/app/styles/vars';
$chat-input-height: 50px;
$chat-input-radius: $radius-large-x;
.ChatFormText {
@include flex-col;
gap: $spacing-small;
}
.ChatFormText__richInput {
@include flex;
position: relative;
width: 100%;
min-height: $chat-input-height;
height: 100%;
align-items: stretch;
&::before {
content: '';
position: absolute;
width: 100%;
}
}
.ChatFormText__input {
border-radius: $chat-input-radius 0 0 $chat-input-radius !important;
flex: 1;
min-height: 50px !important;
}
.ChatFormText__richInputButton {
width: $chat-input-height !important;
height: auto !important;
border-radius: 0 $chat-input-radius $chat-input-radius 0 !important;
padding: 0 !important;
}
.ChatFormText__buttonIcon {
width: 24px;
height: 24px;
}

View File

@ -1,90 +0,0 @@
import { KeyboardEvent } from 'react';
import clsx from 'clsx';
import { Key } from 'ts-key-enum';
import { ReactFCC } from '../../../../../../utils/ReactFCC';
import { Textarea, TextAreaProps } from '../../../../../../components/Textarea';
import { isKey } from '../../../../../../utils/isKey';
import { Button } from '../../../../../../components/Button';
import { ReactComponent as RightIcon } from '../../../../../../assets/icons/right.svg';
import { EntityType, Hint } from '../../../../../../api/deck';
import { Hint as HintCmp, HintsContainer } from '../../../../../../components/Hint';
import { Input, InputProps } from '../../../../../../components/Input';
import s from './ChatFormText.module.scss';
export interface ChatFormTextProps {
className?: string;
onSubmit: (e: any) => void;
registration?: TextAreaProps['registration'];
setValue: (value: any) => void;
hint?: Hint | false;
type: InputProps['type'] | 'textarea';
}
export const ChatFormText: ReactFCC<ChatFormTextProps> = (props) => {
const { className, onSubmit, registration, hint, setValue, type } = props;
return (
<div className={s.ChatFormText}>
<HintsContainer isLoading={hint && !hint.value}>
{hint && hint.value && (
<>
{Array.isArray(hint.value) ? (
hint.type === EntityType.cards ? (
<HintCmp onClick={() => setValue(hint.value.map((i: any) => i.name).join(', '))}>
{hint.value.map((i, index, { length: arrLength }) => (
<span title={i.description} key={index}>
{i.name}
{index !== arrLength - 1 && ', '}
</span>
))}
</HintCmp>
) : (
hint.value.map((item: string, index: number) => (
<HintCmp onClick={() => setValue(item)} key={index}>
{item}
</HintCmp>
))
)
) : (
<HintCmp onClick={() => setValue(hint.value)}>{hint.value}</HintCmp>
)}
</>
)}
</HintsContainer>
<div className={clsx(s.ChatFormText__richInput, className)}>
{type === 'textarea' ? (
<Textarea
className={s.ChatFormText__input}
placeholder={'Введите сообщение'}
rows={1}
onKeyDown={(e: KeyboardEvent) => {
if (isKey(e.nativeEvent, Key.Enter)) {
e.preventDefault();
onSubmit(e);
}
}}
registration={registration}
/>
) : (
<Input
className={s.ChatFormText__input}
type={type}
placeholder={'Введите сообщение'}
onKeyDown={(e: KeyboardEvent) => {
if (isKey(e.nativeEvent, Key.Enter)) {
e.preventDefault();
onSubmit(e);
}
}}
registration={registration}
/>
)}
<Button className={s.ChatFormText__richInputButton} onClick={onSubmit}>
<RightIcon className={s.ChatFormText__buttonIcon} />
</Button>
</div>
</div>
);
};

View File

@ -1 +0,0 @@
export * from './ChatFormText';

View File

@ -1,15 +0,0 @@
@import 'src/app/styles/vars';
.SimpleButton {
width: 100% !important;
@include media-down(tablet-small) {
width: 100% !important;
}
}
.SimpleButton__text {
display: flex;
align-items: center;
column-gap: $spacing-small-3x;
}

View File

@ -1,25 +0,0 @@
import clsx from 'clsx';
import s from './SimpleButton.module.scss';
import {Button} from '../../../../components/Button';
import {ReactFCC} from '../../../../utils/ReactFCC';
import {ReactComponent as RightIcon} from '../../../../assets/icons/right.svg';
export interface SimpleButtonProps {
/**
* Дополнительный css-класс
*/
className?: string;
onClick?: (e: any) => void;
}
export const SimpleButton: ReactFCC<SimpleButtonProps> = (props) => {
const {children, className, onClick} = props;
return (
<Button className={clsx(s.SimpleButton, className)} classes={{text: s.SimpleButton__text}} onClick={onClick}>
{children || 'Вперед!'}
<RightIcon className={s.ChatPage__buttonIcon} />
</Button>
);
};

View File

@ -1 +0,0 @@
export * from './SimpleButton';

View File

@ -1,51 +0,0 @@
import {useEffect, useLayoutEffect, useRef, useState} from 'react';
import {useIsDesktop} from '../../../hooks/useIsDesktop';
export const useChatUi = () => {
const backgroundRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const inputContainerRef = useRef<HTMLDivElement>(null);
const [bgX, setBgX] = useState(50);
const [bgY, setBgY] = useState(50);
const isDesktop = useIsDesktop();
useLayoutEffect(() => {
if (contentRef.current && inputContainerRef.current) {
containerRef.current?.scrollTo({ top: contentRef.current.scrollHeight });
contentRef.current.style.paddingBottom = inputContainerRef.current.scrollHeight + 'px';
}
}, []);
// useEffect(() => {
// function handler(e: MouseEvent) {
// if (isDesktop) {
// const modifierX = 17;
// const modifierY = 5;
//
// setBgX(50 + (-e.clientX / window.innerWidth) * modifierX);
// setBgY(50 + (-e.clientY / window.innerHeight) * modifierY);
// }
// }
//
// window.addEventListener('mousemove', handler);
//
// return () => window.removeEventListener('mousemove', handler);
// }, [isDesktop]);
useEffect(() => {
if (backgroundRef.current) {
backgroundRef.current.style.backgroundPositionX = bgX + '%';
backgroundRef.current.style.backgroundPositionY = bgY + '%';
}
}, [bgX, bgY]);
return {
backgroundRef,
containerRef,
contentRef,
inputContainerRef
}
}

View File

@ -1 +0,0 @@
export * from './ChatPage';

View File

@ -1,3 +0,0 @@
export const useInitChat = () => {
}

View File

@ -1,21 +0,0 @@
import {create} from 'zustand';
export enum ChatItemType {
send = 'send',
receive = 'receive'
}
export type ChatItem = {
type: ChatItemType;
text: string;
}
export interface ChatHistoryStore {
history: ChatItem[];
pushHistory: (item: ChatItem) => void;
}
export const useChatHistory = create<ChatHistoryStore>((set) => ({
history: [],
pushHistory: (item: ChatItem) => set((state) => ({ history: [...state.history, item] })),
}));

View File

@ -1,34 +0,0 @@
import {EntityType, Question} from '../../../api/deck';
export const generateAnswerFromData = (question: Question, data: any) => {
switch (question.type) {
case EntityType.text:
return data.value;
case EntityType.number:
return parseInt(data.value);
case EntityType.date:
return new Date(data.value).toISOString();
case EntityType.link:
return data.value;
case EntityType.select:
return data.value;
case EntityType.multiple_range:
return data.value;
case EntityType.multiple_date_description:
return data.value;
case EntityType.range:
return {
[question.params!.slug]: data.value,
};
case EntityType.multiple_link_description:
return data.value;
case EntityType.photo_description:
return JSON.stringify(data.value.text);
case EntityType.multiple_photo:
return '';
case EntityType.multiple_photo_description:
return JSON.stringify(data.value.map((i: any) => i.text));
default:
return '';
}
}

View File

@ -1,21 +0,0 @@
import { EntityType, Question } from '../../../api/deck';
export const generateFieldsForFileAnswers = (question: Question, data: any) => {
switch (question.type) {
case EntityType.photo_description:
return {
file: data.value.file
};
case EntityType.multiple_photo:
return data.value;
case EntityType.multiple_photo_description:
// eslint-disable-next-line no-case-declarations
const files: any = {};
data.value.forEach((i: any, index: number) => {
files[`file_${index + 1}`] = i.file;
});
return files;
default:
return undefined;
}
};

View File

@ -1,48 +0,0 @@
import { Answer, EntityType } from '../../../api/deck';
import { currencyFormatter, formatDate } from '../../../utils/fomat';
import { slugsForFormat } from '../components/ChatForm/components/ChatFormMultipleRange';
export const generateTextFromAnswer = (type: EntityType, answer: Answer, files?: { [key: string]: File }) => {
switch (type) {
case EntityType.text:
return answer.answer;
case EntityType.number:
return answer.answer;
case EntityType.date:
return formatDate(answer.answer);
case EntityType.link:
return answer.answer;
case EntityType.select:
return answer.answer;
case EntityType.multiple_range:
return Object.entries(answer.answer)
.map(([key, value]: any) => (slugsForFormat.includes(key) ? currencyFormatter.format(value) : value))
.join('\n');
case EntityType.multiple_date_description:
return Object.entries(answer.answer)
.map(([key, value]) => `${formatDate(new Date(key))}: ${value}`)
.join('\n');
case EntityType.range:
// eslint-disable-next-line no-case-declarations
const [slug, value]: any = Object.entries(answer.answer)[0];
return slugsForFormat.includes(slug) ? currencyFormatter.format(value) : value;
case EntityType.multiple_link_description:
return Object.entries(answer.answer)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
case EntityType.photo_description:
return `${answer.answer}\n${files?.file?.name}`;
case EntityType.multiple_photo:
return Object.values(files || {}).map((file) => `${file.name}`);
case EntityType.multiple_photo_description:
// eslint-disable-next-line no-case-declarations
let result = '';
answer.answer.forEach((desc: string, index: number, arr: any) => {
result += files?.[`file_${index + 1}`].name + '\n';
result += desc + (index !== arr.length - 1 ? '\n\n' : '');
});
return result;
default:
return '';
}
};

View File

@ -30,7 +30,6 @@
} }
.TextItem__name { .TextItem__name {
//font-size: $font-size-18;
text-transform: uppercase; text-transform: uppercase;
font-weight: $font-weight-600; font-weight: $font-weight-600;
white-space: nowrap; white-space: nowrap;
@ -40,3 +39,7 @@
margin-top: $spacing-small-3x; margin-top: $spacing-small-3x;
margin-bottom: 0; margin-bottom: 0;
} }
.TextItem__text600 {
font-weight: $font-weight-600;
}

View File

@ -68,7 +68,7 @@ export const TextItem: ReactFCC<TextItemProps> = (props) => {
</div> </div>
<div className={s.TextItem__row}> <div className={s.TextItem__row}>
<Text component={'span'} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}> <Text className={s.TextItem__text600} component={'span'} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
Итоговая оценка: {text.score.total as unknown as string} Итоговая оценка: {text.score.total as unknown as string}
</Text> </Text>
</div> </div>