diff --git a/locales/en.json b/locales/en.json index db39a54..4ef706d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -50,6 +50,12 @@ "dry-wash.order-view.details.location": "Where", "dry-wash.order-view.details.datetime-range": "When", "dry-wash.order-view.details.alert": "The operator will contact you about the payment at the specified phone number", + "dry-wash.order-view.upload-car-image.field.label": "Upload a photo of your car, and our service will quickly calculate the pre-order price!", + "dry-wash.order-view.upload-car-image.field.help": "Allowed formats: .jpg, .png. Maximum size: 5MB", + "dry-wash.order-view.upload-car-image.file-input.placeholder": "Upload a file", + "dry-wash.order-view.upload-car-image.file-input.button": "Upload", + "dry-wash.order-view.upload-car-image-query.success.title": "The car image is successfully uploaded", + "dry-wash.order-view.upload-car-image-query.error.title": "Failed to upload the car image", "dry-wash.arm.master.add": "Add", "dry-wash.arm.order.title": "Orders", "dry-wash.arm.order.table.empty": "Table empty", diff --git a/locales/ru.json b/locales/ru.json index 15586fb..93abaff 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -105,6 +105,12 @@ "dry-wash.order-view.details.location": "Где", "dry-wash.order-view.details.datetime-range": "Когда", "dry-wash.order-view.details.alert": "С вами свяжется оператор насчет оплаты по указанному номеру телефона", + "dry-wash.order-view.upload-car-image.field.label": "Загрузите фото вашего автомобиля, и наш сервис быстро рассчитает предварительную стоимость заказа!", + "dry-wash.order-view.upload-car-image.field.help": "Допустимые форматы: .jpg, .png. Максимальный размер: 5МБ", + "dry-wash.order-view.upload-car-image.file-input.placeholder": "Загрузите файл", + "dry-wash.order-view.upload-car-image.file-input.button": "Загрузить", + "dry-wash.order-view.upload-car-image-query.success.title": "Изображение автомобиля успешно загружено", + "dry-wash.order-view.upload-car-image-query.error.title": "Не удалось загрузить изображение автомобиля", "dry-wash.notFound.title": "Страница не найдена", "dry-wash.notFound.description": "К сожалению, запрашиваемая вами страница не существует.", "dry-wash.notFound.button.back": "Вернуться на главную", diff --git a/src/__data__/service/landing.api.ts b/src/__data__/service/landing.api.ts index 7f84aef..9372cf8 100644 --- a/src/__data__/service/landing.api.ts +++ b/src/__data__/service/landing.api.ts @@ -1,4 +1,4 @@ -import { GetOrder, CreateOrder } from "../../models/api"; +import { GetOrder, CreateOrder, UploadCarImage } from "../../models/api"; import { api } from "./api"; import { extractBodyFromResponse, extractErrorMessageFromResponse } from "./utils"; @@ -19,5 +19,13 @@ export const landingApi = api.injectEndpoints({ transformResponse: extractBodyFromResponse, transformErrorResponse: extractErrorMessageFromResponse, }), + uploadCarImage: mutation({ + query: ({ orderId, body }) => ({ + url: `/order/${orderId}/upload-car-img`, + body, + method: 'POST' + }), + transformErrorResponse: extractErrorMessageFromResponse, + }), }) }); diff --git a/src/__data__/service/utils.ts b/src/__data__/service/utils.ts index 2ffe971..f55bb9c 100644 --- a/src/__data__/service/utils.ts +++ b/src/__data__/service/utils.ts @@ -13,9 +13,10 @@ export const extractErrorMessageFromResponse = ({ }: FetchBaseQueryError) => { if ( typeof data === 'object' && - 'message' in data && - typeof data.message === 'string' + data !== null && + 'error' in data && + typeof data.error === 'string' ) { - return data.message; + return data.error; } }; diff --git a/src/components/order-view/car-img/car-img-form.tsx b/src/components/order-view/car-img/car-img-form.tsx new file mode 100644 index 0000000..76d84e6 --- /dev/null +++ b/src/components/order-view/car-img/car-img-form.tsx @@ -0,0 +1,102 @@ +import React, { FC, memo, useRef } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { + Button, + FormControl, + FormErrorMessage, + FormHelperText, + FormLabel, + HStack, + Input, +} from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; + +import { landingApi } from '../../../__data__/service/landing.api'; +import { UploadCarImage } from '../../../models/api'; + +import { useHandleUploadCarImageResponse } from './helper'; + +type FormValues = { + carImg: File & { + fileName: string; + }; +}; + +type CarImageFormProps = { + orderId: UploadCarImage.Params['orderId']; +}; + +export const CarImageForm: FC = memo(function CarImageForm({ + orderId, +}) { + const { + handleSubmit, + control, + formState: { errors, isSubmitting }, + } = useForm({ shouldFocusError: true }); + + const [uploadCarImage, uploadCarImageMutation] = + landingApi.useUploadCarImageMutation(); + useHandleUploadCarImageResponse(uploadCarImageMutation); + + const onSubmit = (formData: FormValues) => { + const body = new FormData(); + body.append('file', formData.carImg); + uploadCarImage({ orderId, body }); + }; + + const fileInputRef = useRef(null); + + const { t } = useTranslation('~', { + keyPrefix: 'dry-wash.order-view.upload-car-image', + }); + + return ( +
+ + {t('field.label')} + { + return ( + + { + onChange(event.target.files[0]); + handleSubmit(onSubmit)(); + }} + type='file' + hidden + /> + + + + ); + }} + /> + {errors.carImg?.message} + {t('field.help')} + +
+ ); +}); diff --git a/src/components/order-view/car-img/helper.ts b/src/components/order-view/car-img/helper.ts new file mode 100644 index 0000000..f299213 --- /dev/null +++ b/src/components/order-view/car-img/helper.ts @@ -0,0 +1,35 @@ +import { useEffect } from "react"; +import { useToast } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; + +import { isErrorMessage } from "../../../models/api"; + +export const useHandleUploadCarImageResponse = (query: { + isSuccess: boolean; + isError: boolean; + error?: unknown; +}) => { + const toast = useToast(); + const { t } = useTranslation('~', { + keyPrefix: 'dry-wash.order-create.upload-car-image-query', + }); + + useEffect(() => { + if (query.isError) { + toast({ + status: 'error', + title: t('error.title'), + description: isErrorMessage(query.error) ? query.error : undefined, + }); + } + }, [query.isError]); + + useEffect(() => { + if (query.isSuccess) { + toast({ + status: 'success', + title: t('success.title'), + }); + } + }, [query.isSuccess]); +}; \ No newline at end of file diff --git a/src/components/order-view/car-img/index.ts b/src/components/order-view/car-img/index.ts new file mode 100644 index 0000000..4a829cf --- /dev/null +++ b/src/components/order-view/car-img/index.ts @@ -0,0 +1 @@ +export { CarImageForm } from './car-img-form'; diff --git a/src/components/order-view/details/order-details.tsx b/src/components/order-view/details/order-details.tsx index 62c8563..9411404 100644 --- a/src/components/order-view/details/order-details.tsx +++ b/src/components/order-view/details/order-details.tsx @@ -5,11 +5,13 @@ import { Heading, HStack, UnorderedList, - VStack, ListItem, Text, } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; +import dayjs from 'dayjs'; +import localizedFormat from "dayjs/plugin/localizedFormat"; +dayjs.extend(localizedFormat); import { Order } from '../../../models/landing'; import { formatDatetime } from '../../../lib'; @@ -41,7 +43,7 @@ export const OrderDetails: FC = ({ location, startWashTime, endWashTime, - ...props + created }) => { const { t } = useTranslation('~', { keyPrefix: 'dry-wash.order-view.details', @@ -51,7 +53,7 @@ export const OrderDetails: FC = ({ }); return ( - + <> = ({ gap={2} > - {t('title', { number: orderNumber })} + {t('title', { number: orderNumber })} ({dayjs(created).format('LLLL')}) @@ -105,7 +107,7 @@ export const OrderDetails: FC = ({ {t('alert')} - + ); }; diff --git a/src/models/api/common.ts b/src/models/api/common.ts index 4bde4dd..16d573f 100644 --- a/src/models/api/common.ts +++ b/src/models/api/common.ts @@ -9,7 +9,7 @@ export const isErrorMessage = (error: unknown): error is ErrorMessage => typeof type ErrorResponse = { success: false; - message: ErrorMessage; + error: ErrorMessage; }; export type BaseResponse = SuccessResponse | ErrorResponse; diff --git a/src/models/api/order.ts b/src/models/api/order.ts index dabe6f9..71e6503 100644 --- a/src/models/api/order.ts +++ b/src/models/api/order.ts @@ -21,6 +21,17 @@ export namespace CreateOrder { }; } +export namespace UploadCarImage { + export type Response = void; + export type Params = { + orderId: Order.Id; + /** + * @example { file: File } + */ + body: FormData; + }; +} + type GetArrItemType = ArrType extends Array ? ItemType : never; diff --git a/src/pages/__tests__/__snapshots__/order-view.test.tsx.snap b/src/pages/__tests__/__snapshots__/order-view.test.tsx.snap index fabd24b..72a1240 100644 --- a/src/pages/__tests__/__snapshots__/order-view.test.tsx.snap +++ b/src/pages/__tests__/__snapshots__/order-view.test.tsx.snap @@ -16,7 +16,6 @@ exports[`Страница просмотра заказа отображает
Заказ №{{number}} + ( + Sunday, January 19, 2025 5:04 PM + ) С вами свяжется оператор насчет оплаты по указанному номеру телефона
+
+
+ +
+ + + +
+
+ Допустимые форматы: .jpg, .png. Максимальный размер: 5МБ +
+
+
diff --git a/src/pages/__tests__/__snapshots__/ordersList.test.tsx.snap b/src/pages/__tests__/__snapshots__/ordersList.test.tsx.snap index 25091d5..f50f76a 100644 --- a/src/pages/__tests__/__snapshots__/ordersList.test.tsx.snap +++ b/src/pages/__tests__/__snapshots__/ordersList.test.tsx.snap @@ -75,7 +75,7 @@ exports[`Страница заказов должна корректно ото

- 20.02.2025 + 23.02.2025