добавил загрузку фото

This commit is contained in:
Pavel Torbeev 2023-08-27 04:11:28 +03:00
parent 903461baa7
commit 7b04b42964
21 changed files with 426 additions and 20 deletions

View File

@ -9,6 +9,8 @@ export type CreateAnswerDTO = {
deckId: number; deckId: number;
questionId: number; questionId: number;
answer: any; answer: any;
isFile?: boolean;
file?: File;
}; };
export type CreateAnswerResponse = Answer; export type CreateAnswerResponse = Answer;
@ -18,7 +20,29 @@ export const createAnswer = (data: CreateAnswerDTO): Promise<CreateAnswerRespons
.replace(`:${QUESTION_PARAM_DECK_ID}`, String(data.deckId)) .replace(`:${QUESTION_PARAM_DECK_ID}`, String(data.deckId))
.replace(`:${QUESTION_PARAM_QUESTION_ID}`, String(data.questionId)) + '/' .replace(`:${QUESTION_PARAM_QUESTION_ID}`, String(data.questionId)) + '/'
return axios.post(path, { answer: data.answer }); 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') {
// @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 = { type UseCreateAnswerOptions = {

View File

@ -1,17 +1,17 @@
export enum EntityType { export enum EntityType {
text = 'text', // text = 'text', // +
number = 'number', // number = 'number', // +
range = 'range', // range = 'range', // +
multiple_range = 'multiple_range', // multiple_range = 'multiple_range', // +
select = 'select', // select = 'select', // +
link = 'link', // добавить валидацию link = 'link', // + добавить валидацию
date = 'date', // добавить правильную установку хинта date = 'date', // +
photo = 'photo', // photo = 'photo',
multiple_photo = 'multiple_photo', multiple_photo = 'multiple_photo',
photo_description = 'photo_description', photo_description = 'photo_description',
multiple_link_description = 'multiple_link_description', multiple_link_description = 'multiple_link_description', //
multiple_photo_description = 'multiple_photo_description', multiple_photo_description = 'multiple_photo_description',
multiple_links = 'multiple_links', // multiple_links = 'multiple_links', // не пригодилось
multiple_date_description = 'multiple_date_description', // multiple_date_description = 'multiple_date_description', //
text_array = 'text_array', // используется только в подсказке text_array = 'text_array', // используется только в подсказке
cards = 'cards', // используется только в подсказке cards = 'cards', // используется только в подсказке

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 14.25C3.41421 14.25 3.75 14.5858 3.75 15C3.75 16.4354 3.75159 17.4365 3.85315 18.1919C3.9518 18.9257 4.13225 19.3142 4.40901 19.591C4.68577 19.8678 5.07435 20.0482 5.80812 20.1469C6.56347 20.2484 7.56459 20.25 9 20.25H15C16.4354 20.25 17.4365 20.2484 18.1919 20.1469C18.9257 20.0482 19.3142 19.8678 19.591 19.591C19.8678 19.3142 20.0482 18.9257 20.1469 18.1919C20.2484 17.4365 20.25 16.4354 20.25 15C20.25 14.5858 20.5858 14.25 21 14.25C21.4142 14.25 21.75 14.5858 21.75 15V15.0549C21.75 16.4225 21.75 17.5248 21.6335 18.3918C21.5125 19.2919 21.2536 20.0497 20.6517 20.6516C20.0497 21.2536 19.2919 21.5125 18.3918 21.6335C17.5248 21.75 16.4225 21.75 15.0549 21.75H8.94513C7.57754 21.75 6.47522 21.75 5.60825 21.6335C4.70814 21.5125 3.95027 21.2536 3.34835 20.6517C2.74643 20.0497 2.48754 19.2919 2.36652 18.3918C2.24996 17.5248 2.24998 16.4225 2.25 15.0549C2.25 15.0366 2.25 15.0183 2.25 15C2.25 14.5858 2.58579 14.25 3 14.25Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2.25C12.2106 2.25 12.4114 2.33852 12.5535 2.49392L16.5535 6.86892C16.833 7.17462 16.8118 7.64902 16.5061 7.92852C16.2004 8.20802 15.726 8.18678 15.4465 7.88108L12.75 4.9318V16C12.75 16.4142 12.4142 16.75 12 16.75C11.5858 16.75 11.25 16.4142 11.25 16V4.9318L8.55353 7.88108C8.27403 8.18678 7.79963 8.20802 7.49393 7.92852C7.18823 7.64902 7.16698 7.17462 7.44648 6.86892L11.4465 2.49392C11.5886 2.33852 11.7894 2.25 12 2.25Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,17 @@
@import 'src/app/styles/vars';
.UploadButton {
width: 100%;
height: 80px !important;
//background-color: $color-on-surface-dark-400;
}
.UploadButton__icon {
width: 24px;
height: 24px;
color: white;
}
.UploadButton__input {
display: none;
}

View File

@ -0,0 +1,35 @@
import clsx from 'clsx';
import s from './UploadButton.module.scss';
import {ReactFCC} from '../../utils/ReactFCC';
import {Button, ButtonVariant} from '../Button';
import { ReactComponent as UploadIcon } from '../../assets/icons/upload.svg';
import {useId} from 'react';
export interface UploadButtonProps {
/**
* Дополнительный css-класс
*/
className?: string;
onChange?: (e: any) => void;
multiple?: boolean;
}
export const UploadButton: ReactFCC<UploadButtonProps> = (props) => {
const {className, onChange, multiple} = props;
const id = useId();
return (
<Button component={'label'} htmlFor={id} className={clsx(s.UploadButton, className)} variant={ButtonVariant.secondary}>
<UploadIcon className={s.UploadButton__icon} />
<input
className={s.UploadButton__input}
type="file"
id={id}
onChange={onChange}
multiple={multiple}
/>
</Button>
);
};

View File

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

View File

@ -18,6 +18,7 @@ import {generateTextFromAnswer} from './utils/generateTextFromAnswer';
import {Button, ButtonVariant} from '../../components/Button'; import {Button, ButtonVariant} from '../../components/Button';
import {useNavigate} from 'react-router-dom'; import {useNavigate} from 'react-router-dom';
import {DECK_PAGE_PARAM, DECK_PAGE_ROUTE} from '../../app/routes'; import {DECK_PAGE_PARAM, DECK_PAGE_ROUTE} from '../../app/routes';
import {generateFieldsForFileAnswers} from './utils/generateFieldsForFileAnswers';
export interface ChatPageProps { export interface ChatPageProps {
/** /**
@ -28,8 +29,8 @@ export interface ChatPageProps {
const QUESTION_POLLING_MS = 1000; const QUESTION_POLLING_MS = 1000;
const DEFAULT_DECK_ID = 0; const DEFAULT_DECK_ID = 80;
const DEFAULT_QUESTION_ID = 0; const DEFAULT_QUESTION_ID = 25;
export const ChatPage: ReactFCC<ChatPageProps> = (props) => { export const ChatPage: ReactFCC<ChatPageProps> = (props) => {
const {className} = props; const {className} = props;
@ -127,16 +128,19 @@ export const ChatPage: ReactFCC<ChatPageProps> = (props) => {
timeout.clear(); timeout.clear();
const answerValue = generateAnswerFromData(question, data); const answerValue = generateAnswerFromData(question, data);
const additionalFields = generateFieldsForFileAnswers(question, data);
const answer = await createAnswer({ const answer = await createAnswer({
deckId, deckId,
questionId, questionId,
answer: answerValue answer: answerValue,
...additionalFields,
isFile: !!additionalFields && Object.keys(additionalFields).length !== 0
}); });
pushHistory({ pushHistory({
type: ChatItemType.send, type: ChatItemType.send,
text: generateTextFromAnswer(question.type, answer) text: generateTextFromAnswer(question.type, answer, additionalFields)
}); });
if (question.next_id) { if (question.next_id) {

View File

@ -1,12 +1,15 @@
import {ReactFCC} from '../../../../utils/ReactFCC'; import {ReactFCC} from '../../../../utils/ReactFCC';
import {EntityType, Hint, Question} from '../../../../api/deck'; import {EntityType, Question} from '../../../../api/deck';
import {Form} from '../../../../components/Form'; import {Form} from '../../../../components/Form';
import {ChatFormText} from './components/ChatFormText/ChatFormText'; import {ChatFormText} from './components/ChatFormText';
import {ChatFormSelect} from './components/ChatFormSelect'; import {ChatFormSelect} from './components/ChatFormSelect';
import {ChatFormMultipleRange, RangeType} from './components/ChatFormMultipleRange'; import {ChatFormMultipleRange, RangeType} from './components/ChatFormMultipleRange';
import {ChatFormMultipleDateDescription} from './components/ChatFormMultipleDateDescription'; import {ChatFormMultipleDateDescription} from './components/ChatFormMultipleDateDescription';
import {ChatFormRange} from './components/ChatFormRange'; import {ChatFormRange} from './components/ChatFormRange';
import {ChatFormMultipleLinkDescription} from './components/ChatFormMultipleLinkDescription'; import {ChatFormMultipleLinkDescription} from './components/ChatFormMultipleLinkDescription';
import {ChatFormPhotoDescription} from './components/ChatFormPhotoDescription';
import {ChatFormMultiplePhoto} from './components/ChatFormMultiplePhoto';
import {ChatFormMultiplePhotoDescription} from './components/ChatFormMultiplePhotoDescription';
export interface QuestionFactoryProps { export interface QuestionFactoryProps {
type: EntityType; type: EntityType;
@ -183,6 +186,57 @@ export const QuestionFactory: ReactFCC<QuestionFactoryProps> = (props) => {
)} )}
</Form> </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: default:
return null; return null;
} }

View File

@ -32,3 +32,7 @@
.ChatFormMultipleDateDescription__hint { .ChatFormMultipleDateDescription__hint {
white-space: pre-wrap; white-space: pre-wrap;
} }
.ChatFormMultipleDateDescription__itemName {
align-self: flex-start;
}

View File

@ -58,7 +58,11 @@ export const ChatFormMultipleDateDescription: ReactFCC<ChatFormMultipleDateDescr
onChange({ ...newValue, [new Date(e.target.value).toISOString()]: text }) onChange({ ...newValue, [new Date(e.target.value).toISOString()]: text })
}} }}
/> />
<Textarea className={s.ChatFormMultipleDateDescription__textarea} placeholder={'Текст'} value={text} <Textarea
rows={1}
className={s.ChatFormMultipleDateDescription__textarea}
placeholder={'Текст'}
value={text}
onChange={(e) => { onChange={(e) => {
onChange({ ...value, [date]: e.target.value }) onChange({ ...value, [date]: e.target.value })
}} }}

View File

@ -52,7 +52,9 @@ export const ChatFormMultipleLinkDescription: ReactFCC<ChatFormMultipleLinkDescr
onChange({ ...newValue, [e.target.value]: text }) onChange({ ...newValue, [e.target.value]: text })
}} }}
/> />
<Textarea className={s.ChatFormMultipleDateDescription__textarea} <Textarea
rows={1}
className={s.ChatFormMultipleDateDescription__textarea}
placeholder={'Текст'} placeholder={'Текст'}
value={text} value={text}
onChange={(e) => { onChange={(e) => {

View File

@ -0,0 +1,51 @@
import {ReactFCC} from '../../../../../../utils/ReactFCC';
import {ChangeEvent} from 'react';
import s from '../ChatFormPhotoDescription/ChatFormPhotoDescription.module.scss';
import {UploadButton} from '../../../../../../components/UploadButton';
import {Control, Controller, FieldValues, UseFormRegisterReturn} from 'react-hook-form';
import {SimpleButton} from '../../../SimpleButton';
import clsx from 'clsx';
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>
))}
<UploadButton
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;
});
console.log(files);
onChange({ ...files })
}}
/>
<SimpleButton onClick={onSubmit} />
</>
)} name={registration.name!} />
</div>
);
};

View File

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

View File

@ -0,0 +1,73 @@
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 {Textarea} from '../../../../../../components/Textarea';
import {Button, ButtonVariant} from '../../../../../../components/Button';
import {ChangeEvent} from 'react';
import {UploadButton} from '../../../../../../components/UploadButton';
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>
<UploadButton 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

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

View File

@ -0,0 +1,40 @@
@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

@ -0,0 +1,52 @@
import {ReactFCC} from '../../../../../../utils/ReactFCC';
import {Textarea} from '../../../../../../components/Textarea';
import {ChangeEvent} from 'react';
import s from './ChatFormPhotoDescription.module.scss';
import {UploadButton} from '../../../../../../components/UploadButton';
import {Control, Controller, FieldValues, UseFormRegisterReturn} from 'react-hook-form';
import {SimpleButton} from '../../../SimpleButton';
import clsx from 'clsx';
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>
)}
<UploadButton 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

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

View File

@ -22,6 +22,12 @@ export const generateAnswerFromData = (question: Question, data: any) => {
}; };
case EntityType.multiple_link_description: case EntityType.multiple_link_description:
return data.value; 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: default:
return ''; return '';
} }

View File

@ -0,0 +1,20 @@
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:
const files: any = {};
data.value.forEach((i: any, index: number) => {
files[`file_${index + 1}`] = i.file;
});
return files;
default:
return undefined;
}
}

View File

@ -2,7 +2,7 @@ import {Answer, EntityType} from '../../../api/deck';
import {currencyFormatter, formatDate} from '../../../utils/fomat'; import {currencyFormatter, formatDate} from '../../../utils/fomat';
import {slugsForFormat} from '../components/ChatForm/components/ChatFormMultipleRange'; import {slugsForFormat} from '../components/ChatForm/components/ChatFormMultipleRange';
export const generateTextFromAnswer = (type: EntityType, answer: Answer) => { export const generateTextFromAnswer = (type: EntityType, answer: Answer, files?: { [key: string]: File }) => {
switch (type) { switch (type) {
case EntityType.text: case EntityType.text:
return answer.answer; return answer.answer;
@ -25,6 +25,17 @@ export const generateTextFromAnswer = (type: EntityType, answer: Answer) => {
currencyFormatter.format(value) : value currencyFormatter.format(value) : value
case EntityType.multiple_link_description: case EntityType.multiple_link_description:
return Object.entries(answer.answer).map(([key, value]) => `${key}: ${value}`).join('\n'); 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:
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: default:
return ''; return '';
} }