Merge pull request 'feature/order-form-upgrade' (#82) from feature/order-form-upgrade into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good

Reviewed-on: #82
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
This commit is contained in:
Primakov Alexandr Alexandrovich 2025-02-09 10:09:45 +03:00
commit f6cc2efb86
16 changed files with 124 additions and 131 deletions

View File

@ -39,6 +39,7 @@
"dry-wash.order-create.car-body-select.options.sports-car" : "Sports-car", "dry-wash.order-create.car-body-select.options.sports-car" : "Sports-car",
"dry-wash.order-create.car-body-select.options.other": "Other", "dry-wash.order-create.car-body-select.options.other": "Other",
"dry-wash.order-create.form.submit-button.label": "Submit", "dry-wash.order-create.form.submit-button.label": "Submit",
"dry-wash.order-create.order-creation-title": "Creating order ...",
"dry-wash.order-create.create-order-query.success.title": "The order is successfully created", "dry-wash.order-create.create-order-query.success.title": "The order is successfully created",
"dry-wash.order-create.create-order-query.error.title": "Failed to create an order", "dry-wash.order-create.create-order-query.error.title": "Failed to create an order",
"dry-wash.order-view.title": "Your order", "dry-wash.order-view.title": "Your order",

View File

@ -88,6 +88,7 @@
"dry-wash.order-create.car-body-select.options.sports-car": "Спорткар", "dry-wash.order-create.car-body-select.options.sports-car": "Спорткар",
"dry-wash.order-create.car-body-select.options.other": "Другой", "dry-wash.order-create.car-body-select.options.other": "Другой",
"dry-wash.order-create.form.submit-button.label": "Отправить", "dry-wash.order-create.form.submit-button.label": "Отправить",
"dry-wash.order-create.order-creation-title": "Создаем заказ ...",
"dry-wash.order-create.create-order-query.success.title": "Заказ успешно создан", "dry-wash.order-create.create-order-query.success.title": "Заказ успешно создан",
"dry-wash.order-create.create-order-query.error.title": "Не удалось создать заказ", "dry-wash.order-create.create-order-query.error.title": "Не удалось создать заказ",
"dry-wash.order-view.title": "Ваш заказ", "dry-wash.order-view.title": "Ваш заказ",

View File

@ -1,14 +1,23 @@
import { GetOrder } from "../../models/api"; import { GetOrder, CreateOrder } from "../../models/api";
import { api } from "./api"; import { api } from "./api";
import { extractBodyFromResponse, extractErrorMessageFromResponse } from "./utils"; import { extractBodyFromResponse, extractErrorMessageFromResponse } from "./utils";
export const landingApi = api.injectEndpoints({ export const landingApi = api.injectEndpoints({
endpoints: ({ query }) => ({ endpoints: ({ mutation, query }) => ({
getOrder: query<GetOrder.Response, GetOrder.Params>({ getOrder: query<GetOrder.Response, GetOrder.Params>({
query: ({ orderId }) => `/order/${orderId}`, query: ({ orderId }) => `/order/${orderId}`,
transformResponse: extractBodyFromResponse<GetOrder.Response>, transformResponse: extractBodyFromResponse<GetOrder.Response>,
transformErrorResponse: extractErrorMessageFromResponse, transformErrorResponse: extractErrorMessageFromResponse,
}) }),
createOrder: mutation<CreateOrder.Response, CreateOrder.Params>({
query: ({ body }) => ({
url: `/order/create`,
params: { body },
method: 'POST'
}),
transformResponse: extractBodyFromResponse<CreateOrder.Response>,
transformErrorResponse: extractErrorMessageFromResponse,
}),
}) })
}); });

View File

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

View File

@ -1,65 +0,0 @@
import { getConfigValue } from '@brojs/cli';
import { useState } from 'react';
import { CreateOrder } from '../models/api';
import { QueryState, Trigger } from './types';
enum LandingEndpoints {
ORDER_CREATE = '/order/create',
}
const endpoint = getConfigValue('dry-wash.api');
const useCreateOrderMutation = <D extends CreateOrder.Response>(): [
Trigger<CreateOrder.Params, QueryState<D>['data']>,
QueryState<D>,
] => {
const [isLoading, setIsLoading] = useState<QueryState<D>['isLoading']>(false);
const [isSuccess, setIsSuccess] = useState<QueryState<D>['isSuccess']>();
const [data, setData] = useState<QueryState<D>['data']>();
const [isError, setIsError] = useState<QueryState<D>['isError']>();
const [error, setError] = useState<QueryState<D>['error']>();
const createOrder = async ({ body }: CreateOrder.Params) => {
setIsLoading(true);
try {
const response = await fetch(
`${endpoint}${LandingEndpoints.ORDER_CREATE}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
},
);
if (!response.ok) {
const errorResponseObject =
(await response.json()) as QueryState<D>['error'];
setIsError(true);
setError(errorResponseObject);
throw errorResponseObject;
}
const dataResponseObject =
(await response.json()) as QueryState<D>['data'];
setIsSuccess(true);
setData(dataResponseObject);
return dataResponseObject;
} catch (error) {
setIsError(true);
setError(error);
throw error;
} finally {
setIsLoading(false);
}
};
return [createOrder, { isLoading, isSuccess, data, isError, error }];
};
export { useCreateOrderMutation };

View File

@ -1,22 +0,0 @@
export type QueryData<D> = {
success: true;
body: D;
};
export type QueryErrorData = {
success: false;
error: string;
};
export type QueryState<D> = {
isLoading: boolean;
isSuccess: boolean;
data: QueryData<D>;
isError: boolean;
error: {
status: number;
data: QueryErrorData;
};
};
export type Trigger<P, D> = (params: P) => Promise<D>;

View File

@ -0,0 +1 @@
export { default as OrderCreationAnimation } from './order-creation.json';

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,8 @@ type SuccessResponse<Body> = {
export type ErrorMessage = string; export type ErrorMessage = string;
export const isErrorMessage = (error: unknown): error is ErrorMessage => typeof error === 'string';
type ErrorResponse = { type ErrorResponse = {
success: false; success: false;
message: ErrorMessage; message: ErrorMessage;

View File

@ -1,7 +1,13 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useToast } from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Order } from "../../models/landing"; import { Order } from "../../models/landing";
import { OrderFormValues } from "../../components/order-form"; import { OrderFormValues } from "../../components/order-form";
import { isErrorMessage } from "../../models/api";
import { URLs } from '../../__data__/urls';
const removeAllSpaces = (str: string) => str.replace(/\s+/g, ''); const removeAllSpaces = (str: string) => str.replace(/\s+/g, '');
@ -27,3 +33,39 @@ export const formatFormValues = ({ phone, carNumber, carBody, carColor, carLocat
} }
}; };
}; };
export const useHandleCreateOrderMutationResponse = (query: {
isSuccess: boolean;
data?: {
id: Parameters<typeof URLs.orderView.getUrl>[0];
};
isError: boolean;
error?: unknown;
}) => {
const toast = useToast();
const navigate = useNavigate();
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.order-create.create-order-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) {
const orderId = query.data.id;
navigate({ pathname: URLs.orderView.getUrl(orderId) });
toast({
status: 'success',
title: t('success.title'),
});
}
}, [query.isSuccess]);
};

View File

@ -1,41 +1,29 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Container, Heading, useToast, VStack } from '@chakra-ui/react'; import { Container, Heading, VStack } from '@chakra-ui/react';
import { useNavigate } from 'react-router-dom'; import { Player as LottiePlayer } from '@lottiefiles/react-lottie-player';
import { withLandingThemeProvider } from '../../containers'; import { withLandingThemeProvider } from '../../containers';
import { OrderForm, OrderFormProps } from '../../components/order-form'; import { OrderForm, OrderFormProps } from '../../components/order-form';
import { useCreateOrderMutation } from '../../api'; import { landingApi } from '../../__data__/service/landing.api';
import { URLs } from '../../__data__/urls'; import { OrderCreationAnimation } from '../../assets/animation';
import { formatFormValues } from './helper'; import {
formatFormValues,
useHandleCreateOrderMutationResponse,
} from './helper';
const Page: FC = () => { const Page: FC = () => {
const { t } = useTranslation('~', { const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.order-create', keyPrefix: 'dry-wash.order-create',
}); });
const [createOrder, createOrderMutation] = useCreateOrderMutation(); const [createOrder, createOrderMutation] =
landingApi.useCreateOrderMutation();
const toast = useToast(); useHandleCreateOrderMutationResponse(createOrderMutation);
const navigate = useNavigate();
const onOrderFormSubmit: OrderFormProps['onSubmit'] = (values) => { const onOrderFormSubmit: OrderFormProps['onSubmit'] = (values) => {
createOrder({ body: formatFormValues(values) }) createOrder({ body: formatFormValues(values) });
.then(({ body: { id: orderId } }) => {
navigate({ pathname: URLs.orderView.getUrl(orderId) });
toast({
status: 'success',
title: t('create-order-query.success.title'),
});
})
.catch(({ error: errorMessage }) => {
toast({
status: 'error',
title: t('create-order-query.error.title'),
description: errorMessage,
});
});
}; };
return ( return (
@ -48,13 +36,24 @@ const Page: FC = () => {
centerContent centerContent
> >
<VStack w='full' h='full' alignItems='stretch' flexGrow={1}> <VStack w='full' h='full' alignItems='stretch' flexGrow={1}>
<Heading textAlign='center' mt={4}> {createOrderMutation.isUninitialized ? (
{t('title')} <>
</Heading> <Heading textAlign='center' mt={4}>
<OrderForm {t('title')}
onSubmit={onOrderFormSubmit} </Heading>
loading={createOrderMutation.isLoading} <OrderForm
/> onSubmit={onOrderFormSubmit}
loading={createOrderMutation.isLoading}
/>
</>
) : (
<>
<LottiePlayer autoplay loop src={OrderCreationAnimation} />
<Heading textAlign='center' mt={4}>
{t('order-creation-title')}
</Heading>
</>
)}
</VStack> </VStack>
</Container> </Container>
); );

View File

@ -19,7 +19,7 @@ import {
import { OrderDetails } from '../../components/order-view'; import { OrderDetails } from '../../components/order-view';
import { Order } from '../../models/landing'; import { Order } from '../../models/landing';
import { landingApi } from '../../__data__/service/landing.api'; import { landingApi } from '../../__data__/service/landing.api';
import { ErrorMessage } from '../../models/api'; import { isErrorMessage } from '../../models/api';
import { FEATURE } from '../../__data__/features'; import { FEATURE } from '../../__data__/features';
const Page: FC = () => { const Page: FC = () => {
@ -45,7 +45,7 @@ const Page: FC = () => {
} }
: undefined, : undefined,
); );
const errorMessage = error as ErrorMessage; const errorMessage = isErrorMessage(error) ? error : undefined;
return ( return (
<LandingThemeProvider> <LandingThemeProvider>
@ -91,7 +91,9 @@ const Page: FC = () => {
<AlertTitle> <AlertTitle>
{t('get-order-query.error.title')} {t('get-order-query.error.title')}
</AlertTitle> </AlertTitle>
<AlertDescription>{errorMessage}</AlertDescription> {errorMessage && (
<AlertDescription>{errorMessage}</AlertDescription>
)}
</Box> </Box>
</Alert> </Alert>
)} )}

View File

@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-require-imports */
const router = require('express').Router(); const router = require('express').Router();
const STUBS = { masters: 'success', orders: 'success', orderView: 'success-pending' }; const STUBS = { masters: 'success', orders: 'success', orderCreate: 'success', orderView: 'success-pending' };
router.get('/set/:name/:value', (req, res) => { router.get('/set/:name/:value', (req, res) => {
const { name, value } = req.params; const { name, value } = req.params;
@ -27,6 +27,11 @@ router.get('/', (req, res) => {
${generateRadioInput('orders', 'empty')} ${generateRadioInput('orders', 'empty')}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Лендинг - Сделать заказ</legend>
${generateRadioInput('orderCreate', 'success')}
${generateRadioInput('orderCreate', 'error')}
</fieldset>
<fieldset>
<legend>Лендинг - Детали заказа</legend> <legend>Лендинг - Детали заказа</legend>
${generateRadioInput('orderView', 'success-pending')} ${generateRadioInput('orderView', 'success-pending')}
${generateRadioInput('orderView', 'success-working')} ${generateRadioInput('orderView', 'success-working')}

View File

@ -87,7 +87,15 @@ router.get('/order/:orderId', ({ params }, res) => {
}); });
router.post('/order/create', (req, res) => { router.post('/order/create', (req, res) => {
res.status(200).send({ success: true, body: { ok: true } }); const stubName = STUBS.orderCreate;
res
.status(/error/.test(stubName) ? 500 : 200)
.send(
/^error$/.test(stubName)
? commonError
: require(`../json/landing-order-create/${stubName}.json`),
);
}); });
router.use('/admin', require('./admin')); router.use('/admin', require('./admin'));

View File

@ -0,0 +1,4 @@
{
"success": false,
"message": "Не удалось создать заказ"
}

View File

@ -0,0 +1,6 @@
{
"success": true,
"body": {
"id": "id1"
}
}