Compare commits

..

7 Commits

Author SHA1 Message Date
b5f3838536 feat: separation of imports (#25)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-24 14:54:01 +03:00
dee3a04310 feat: add dynamic routing (#25)
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
it-academy/dry-wash-pl/pipeline/head There was a failure building this commit
2024-11-24 14:50:51 +03:00
c2fad8a3ff Merge pull request 'feat: add multilingualism (#35)' (#39) from feature/i18next-multilingualism into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good
Reviewed-on: #39
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2024-11-24 11:22:09 +03:00
0b30ce6571 feat: divide imports by types (#35)
All checks were successful
it-academy/dry-wash-pl/pipeline/pr-main This commit looks good
2024-11-23 15:37:06 +03:00
3b47e1dcdf Merge remote-tracking branch 'origin/feature/i18next-multilingualism' into feature/i18next-multilingualism
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
2024-11-23 15:28:44 +03:00
4e8bc2fca5 feat: add multilingualism (#35) 2024-11-23 15:27:53 +03:00
0c9663fcef feat: add multilingualism (#35)
Some checks failed
it-academy/dry-wash-pl/pipeline/pr-main There was a failure building this commit
it-academy/dry-wash-pl/pipeline/head This commit looks good
2024-11-23 15:19:44 +03:00
36 changed files with 338 additions and 353 deletions

View File

@@ -45,12 +45,10 @@
### MVP1
**1. Landing**
- преимущества сервиса
- оставить заявку (редирект на Страницу оформления заказа)
**2. Страница для оформления заказа**
- форма
- номер машины (mask input)
- цвет машины
@@ -60,12 +58,10 @@
- после заполнения редирект на страницу с деталями заказа
**3. Страница с деталями заказа**
- описание заказа
- детали заказа (id, статус)
**3. АРМ оператора**
- список заказов (RUD)
- id заказа
- статус заказа (готово / не готово)
@@ -76,6 +72,7 @@
- кнопка "Добавить"
- кнопка "Удалить"
### Built With
[![React][React.js]][React-url]
@@ -106,14 +103,6 @@
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Instructions
### Stubs types generation
1. generate types with json-literal-typer (should be installed globally)
```sh
npx json-literal-typer -i <path to json> -o <path to output ts-file>
```
2. export default type from output file
<!-- PARTICIPANTS -->
## Participants

View File

@@ -1,9 +1,9 @@
/* eslint-disable no-undef */
/* eslint-disable @typescript-eslint/no-require-imports */
const pkg = require("./package");
const pkg = require('./package');
module.exports = {
apiPath: "stubs/api",
apiPath: 'stubs/api',
webpackConfig: {
output: {
publicPath: `/static/${pkg.name}/${process.env.VERSION || pkg.version}/`,
@@ -11,17 +11,19 @@ module.exports = {
},
/* use https://admin.bro-js.ru/ to create config, navigations and features */
navigations: {
"dry-wash.main": "/dry-wash",
"dry-wash.create": "/order",
"dry-wash.view.order": "/order/:orderId",
"dry-wash.arm": "/arm",
'dry-wash.main': '/dry-wash',
'dry-wash.create': '/order',
'dry-wash.view.order': '/order/:orderId',
'dry-wash.arm.master': '/master',
'dry-wash.arm.order': '/order',
'dry-wash.arm': '/arm/*',
},
features: {
"dry-wash-pl": {
'dry-wash-pl': {
// add your features here in the format [featureName]: { value: string }
},
},
config: {
"dry-wash-pl.api": "/api",
'dry-wash-pl.api': '/api',
},
};

View File

@@ -13,5 +13,41 @@
"dry-wash.landing.hero-section.headline": "Revitalize Your Ride with Eco-Friendly Care!",
"dry-wash.landing.make-order-button": "Make order",
"dry-wash.landing.site-logo": "The logo of the \"Dry Master\" company",
"dry-wash.landing.social-proof-section.heading": "We are being chosen"
"dry-wash.landing.social-proof-section.heading": "We are being chosen",
"dry-wash.arm.master.add": "Add",
"dry-wash.arm.order.title": "Orders",
"dry-wash.arm.order.status.progress": "In Progress",
"dry-wash.arm.order.status.complete": "Completed",
"dry-wash.arm.order.status.pending": "Pending",
"dry-wash.arm.order.status.working": "Working",
"dry-wash.arm.order.status.canceled": "Canceled",
"dry-wash.arm.order.status.placeholder": "Select Status",
"dry-wash.arm.order.table.header.carNumber": "Car Number",
"dry-wash.arm.order.table.header.washingTime": "Washing Time",
"dry-wash.arm.order.table.header.orderDate": "Order Date",
"dry-wash.arm.order.table.header.status": "Status",
"dry-wash.arm.order.table.header.telephone": "Telephone",
"dry-wash.arm.order.table.header.location": "Location",
"dry-wash.arm.master.title": "Masters",
"dry-wash.arm.master.table.header.name": "Name",
"dry-wash.arm.master.table.header.currentJob": "Current Job",
"dry-wash.arm.master.table.header.phone": "Phone",
"dry-wash.arm.master.table.header.actions": "Actions",
"dry-wash.arm.master.table.actionsMenu.delete": "Delete Master",
"dry-wash.arm.master.drawer.title": "Add New Master",
"dry-wash.arm.master.drawer.inputName.label": "Full Name",
"dry-wash.arm.master.drawer.inputName.placeholder": "Enter Full Name",
"dry-wash.arm.master.drawer.inputPhone.label": "Phone Number",
"dry-wash.arm.master.drawer.inputPhone.placeholder": "Enter Phone Number",
"dry-wash.arm.master.drawer.button.save": "Save",
"dry-wash.arm.master.drawer.button.cancel": "Cancel",
"dry-wash.arm.master.sideBar.orders": "Orders",
"dry-wash.arm.master.sideBar.master": "Masters",
"dry-wash.arm.master.sideBar.title": "Dry Master",
"dry-wash.notFound.title": "Page Not Found",
"dry-wash.notFound.description": "Unfortunately, the page you are looking for does not exist.",
"dry-wash.notFound.button.back": "Back to Home",
"dry-wash.errorBoundary.title": "Something went wrong",
"dry-wash.errorBoundary.description": "We are already working on fixing the issue",
"dry-wash.errorBoundary.button.reload": "Reload Page"
}

View File

@@ -3,7 +3,7 @@
"dry-wash.arm.order.title": "Заказы",
"dry-wash.arm.order.status.progress": "Выполняется",
"dry-wash.arm.order.status.complete": "Завершено",
"dry-wash.arm.order.status.pending": "в ожидании",
"dry-wash.arm.order.status.pending": "В ожидании",
"dry-wash.arm.order.status.working": "В работе",
"dry-wash.arm.order.status.canceled": "Отменено",
"dry-wash.arm.order.status.placeholder": "Выберите статус",
@@ -26,9 +26,9 @@
"dry-wash.arm.master.drawer.inputPhone.placeholder": "Введите номер телефона",
"dry-wash.arm.master.drawer.button.save": "Сохранить",
"dry-wash.arm.master.drawer.button.cancel": "Отменить",
"dry-wash.arm.master.sideBar.title": " Сухой мастер",
"dry-wash.arm.master.sideBar.title.master": "Мастера",
"dry-wash.arm.master.sideBar.title.orders": "Заказы",
"dry-wash.arm.master.sideBar.orders": "Заказы",
"dry-wash.arm.master.sideBar.master": "Мастера",
"dry-wash.arm.master.sideBar.title": "Сухой мастер",
"dry-wash.landing.benefits-section.description": "Откройте для себя преимущества наших услуг по химчистке автомобилей",
"dry-wash.landing.benefits-section.heading": "Преимущества экологичной автомойки",
"dry-wash.landing.benefits-section.list.0": "Экологически безопасные продукты",
@@ -46,8 +46,8 @@
"dry-wash.landing.social-proof-section.heading": "Нас выбирают",
"dry-wash.notFound.title": "Страница не найдена",
"dry-wash.notFound.description": "К сожалению, запрашиваемая вами страница не существует.",
"dry-wash.notFound.button.back": " Вернуться на главную",
"dry-wash.notFound.button.back": "Вернуться на главную",
"dry-wash.errorBoundary.title":"Что-то пошло не так",
"dry-wash.errorBoundary.description": " Мы уже работаем над исправлением проблемы",
"dry-wash.errorBoundary.description": "Мы уже работаем над исправлением проблемы",
"dry-wash.errorBoundary.button.reload": "Перезагрузить страницу"
}

14
package-lock.json generated
View File

@@ -6958,6 +6958,20 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",

View File

@@ -1,25 +1,40 @@
import { generatePath } from "react-router-dom";
import { getNavigationValue } from "@brojs/cli";
import { generatePath } from 'react-router-dom';
import { getNavigationValue } from '@brojs/cli';
import { Order } from "../models";
import { Order } from '../models';
const getFullUrls = (url: string) =>
`${getNavigationValue('dry-wash.main')}${url}`;
export const URLs = {
landing: {
url: getNavigationValue("dry-wash.main"),
url: getNavigationValue('dry-wash.main'),
getUrl() {
return this.url;
}
},
},
orderForm: {
url: getNavigationValue("dry-wash.create"),
url: getNavigationValue('dry-wash.create'),
getUrl() {
return this.url;
}
},
},
orderView: {
url: getNavigationValue("dry-wash.view.order"),
url: getNavigationValue('dry-wash.view.order'),
getUrl(orderId: Order.Id) {
return generatePath(this.url, { orderId });
}
}
};
},
},
armMaster: {
url: getNavigationValue('dry-wash.arm.master'),
isOn: Boolean(getNavigationValue('dry-wash.arm.master')),
},
armOrder: {
url: getNavigationValue('dry-wash.arm.order'),
isOn: Boolean(getNavigationValue('dry-wash.arm.order')),
},
armBase: {
url: getFullUrls(getNavigationValue('dry-wash.arm')),
isOn: Boolean(getNavigationValue('dry-wash.arm')),
},
};

View File

@@ -34,15 +34,16 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
render() {
const { hasError } = this.state;
//TODO: добавить анимацию после залива 404 страницы
//TODO: может сделать обертку для хука, чтоб язык менялся без перезагрузки
if (hasError) {
return (
<Center minH='100vh'>
<VStack spacing={4} textAlign='center'>
<Heading as='h1' size='2xl'>
{i18next.t('dry-wash.errorBoundary.title')}
{i18next.t('~:dry-wash.errorBoundary.title')}
</Heading>
<Text fontSize='lg'>
{i18next.t('dry-wash.errorBoundary.description')}
{i18next.t('~:dry-wash.errorBoundary.description')}
</Text>
<Button
colorScheme='teal'
@@ -50,7 +51,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
variant='outline'
onClick={() => window.location.reload()}
>
{i18next.t('dry-wash.errorBoundary.button.reload')}
{i18next.t('~:dry-wash.errorBoundary.button.reload')}
</Button>
</VStack>
</Center>

View File

@@ -5,20 +5,33 @@ import { Navigate, Route, Routes } from 'react-router-dom';
import Sidebar from '../Sidebar';
import Orders from '../Orders';
import Masters from '../Masters';
import { URLs } from '../../__data__/urls';
const LayoutArm = () => (
<Flex h='100vh'>
<Sidebar />
<Box flex='1' bg='gray.50'>
<Routes>
<Route>
<Route index element={<Navigate to='orders' replace />} />
<Route path='orders' element={<Orders />} />
<Route path='masters' element={<Masters />} />
</Route>
</Routes>
</Box>
</Flex>
);
const LayoutArm = () => {
let defaultRedirect = null;
if (URLs.armOrder.isOn) {
defaultRedirect = URLs.armOrder.url;
} else if (URLs.armMaster.isOn) {
defaultRedirect = URLs.armMaster.url;
}
return (
<Flex h='100vh'>
<Sidebar />
<Box flex='1' bg='gray.50'>
<Routes>
<Route index element={<Navigate to={defaultRedirect} replace />} />
{URLs.armOrder.isOn && (
<Route path={URLs.armOrder.url} element={<Orders />} />
)}
{URLs.armMaster.isOn && (
<Route path={URLs.armMaster.url} element={<Masters />} />
)}
</Routes>
</Box>
</Flex>
);
};
export default LayoutArm;

View File

@@ -7,16 +7,18 @@ import {
IconButton,
} from '@chakra-ui/react';
import { EditIcon } from '@chakra-ui/icons';
import i18next from 'i18next';
import { useTranslation } from 'react-i18next';
const MasterActionsMenu = () => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master.table.actionsMenu',
});
return (
<Menu>
<MenuButton icon={<EditIcon />} as={IconButton} variant='outline' />
<MenuList>
<MenuItem>
{i18next.t('dry-wash.arm.master.table.actionsMenu.delete')}
</MenuItem>
<MenuItem>{t('delete')}</MenuItem>
</MenuList>
</Menu>
);

View File

@@ -12,7 +12,7 @@ import {
DrawerHeader,
DrawerOverlay,
} from '@chakra-ui/react';
import i18next from 'i18next';
import { useTranslation } from 'react-i18next';
const MasterDrawer = ({ isOpen, onClose }) => {
const [newMaster, setNewMaster] = useState({ name: '', phone: '' });
@@ -22,51 +22,44 @@ const MasterDrawer = ({ isOpen, onClose }) => {
onClose();
};
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master.drawer',
});
return (
<Drawer isOpen={isOpen} onClose={onClose} size='md'>
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
<DrawerHeader>
{i18next.t('dry-wash.arm.master.drawer.title')}
</DrawerHeader>
<DrawerHeader>{t('title')}</DrawerHeader>
<DrawerBody>
<FormControl mb='4'>
<FormLabel>
{i18next.t('dry-wash.arm.master.drawer.inputName.label')}
</FormLabel>
<FormLabel>{t('inputName.label')}</FormLabel>
<Input
value={newMaster.name}
onChange={(e) =>
setNewMaster({ ...newMaster, name: e.target.value })
}
placeholder={i18next.t(
'dry-wash.arm.master.drawer.inputName.placeholder',
)}
placeholder={t('inputName.placeholder')}
/>
</FormControl>
<FormControl>
<FormLabel>
{' '}
{i18next.t('dry-wash.arm.master.drawer.inputPhone.label')}
</FormLabel>
<FormLabel> {t('inputPhone.label')}</FormLabel>
<Input
value={newMaster.phone}
onChange={(e) =>
setNewMaster({ ...newMaster, phone: e.target.value })
}
placeholder={i18next.t(
'dry-wash.arm.master.drawer.inputPhone.placeholder',
)}
placeholder={t('inputPhone.placeholder')}
/>
</FormControl>
</DrawerBody>
<DrawerFooter>
<Button colorScheme='teal' mr={3} onClick={handleSave}>
{i18next.t('dry-wash.arm.master.drawer.button.save')}
{t('button.save')}
</Button>
<Button variant='ghost' onClick={onClose}>
{i18next.t('dry-wash.arm.master.drawer.button.cancel')}
{t('button.cancel')}
</Button>
</DrawerFooter>
</DrawerContent>

View File

@@ -11,32 +11,39 @@ import {
useDisclosure,
Flex,
} from '@chakra-ui/react';
import i18next from 'i18next';
import { useTranslation } from 'react-i18next';
import { mastersData } from '../../mocks';
import MasterItem from '../MasterItem';
import MasterDrawer from '../MasterDrawer';
const TABLE_HEADERS = ['name', 'currentJob', 'phone', 'actions'];
const TABLE_HEADERS = [
'name' as const,
'currentJob' as const,
'phone' as const,
'actions' as const,
];
const Masters = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master',
});
return (
<Box p='8'>
<Flex justifyContent='space-between' alignItems='center' mb='5'>
<Heading size='lg'> {i18next.t('dry-wash.arm.master.title')}</Heading>
<Heading size='lg'> {t('title')}</Heading>
<Button colorScheme='green' onClick={onOpen}>
+ {i18next.t('dry-wash.arm.master.add')}
+ {t('add')}
</Button>
</Flex>
<Table variant='simple' colorScheme='blackAlpha'>
<Thead>
<Tr>
{TABLE_HEADERS.map((name) => (
<Th key={name}>
{i18next.t(`dry-wash.arm.master.table.header.${name}`)}
</Th>
<Th key={name}>{t(`table.header.${name}`)}</Th>
))}
</Tr>
</Thead>

View File

@@ -1,8 +1,26 @@
import React, { useState } from 'react';
import { Td, Tr, Link, Select } from '@chakra-ui/react';
import i18next from 'i18next';
import { useTranslation } from 'react-i18next';
const statuses = ['pending', 'progress', 'working', 'canceled', 'complete'];
const statuses = [
'pending' as const,
'progress' as const,
'working' as const,
'canceled' as const,
'complete' as const,
];
type GetArrItemType<ArrType> =
ArrType extends Array<infer ItemType> ? ItemType : never;
export type OrderProps = {
carNumber?: string;
washTime?: string;
orderDate?: string;
status?: GetArrItemType<typeof statuses>;
phone?: string;
location?: string;
};
const OrderItem = ({
carNumber,
@@ -11,7 +29,11 @@ const OrderItem = ({
status,
phone,
location,
}) => {
}: OrderProps) => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.order',
});
const [statusSelect, setStatus] = useState(status);
return (
@@ -22,12 +44,12 @@ const OrderItem = ({
<Td>
<Select
value={statusSelect}
onChange={(e) => setStatus(e.target.value)}
placeholder={i18next.t(`dry-wash.arm.order.status.placeholder`)}
onChange={(e) => setStatus(e.target.value as OrderProps['status'])}
placeholder={t(`status.placeholder`)}
>
{statuses.map((status) => (
<option key={status} value={status}>
{i18next.t(`dry-wash.arm.order.status.${status}`)}
{t(`status.${status}`)}
</option>
))}
</Select>

View File

@@ -1,36 +1,45 @@
import { Box, Heading, Table, Thead, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import i18next from 'i18next';
import { Box, Heading, Table, Thead, Tbody, Tr, Th } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { ordersData } from '../../mocks';
import OrderItem from '../OrderItem';
import { OrderProps } from '../OrderItem/OrderItem';
const Orders = () => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.order',
});
const TABLE_HEADERS = [
'carNumber',
'washingTime',
'orderDate',
'status',
'telephone',
'location',
'carNumber' as const,
'washingTime' as const,
'orderDate' as const,
'status' as const,
'telephone' as const,
'location' as const,
];
return (
<Box p='8'>
<Heading size='lg' mb='5'>
{i18next.t('dry-wash.arm.order.title')}
{t('title')}
</Heading>
<Table variant='simple' colorScheme='blackAlpha'>
<Thead>
<Tr>
{TABLE_HEADERS.map((name, key) => (
<Th key={key}>
{i18next.t(`dry-wash.arm.order.table.header.${name}`)}
</Th>
<Th key={key}>{t(`table.header.${name}`)}</Th>
))}
</Tr>
</Thead>
<Tbody>
{ordersData.map((order, index) => (
<OrderItem key={index} {...order} />
<OrderItem
key={index}
{...order}
status={order.status as OrderProps['status']}
/>
))}
</Tbody>
</Table>

View File

@@ -1,46 +1,57 @@
import { Box, Button, Heading, VStack } from '@chakra-ui/react';
import { Box, Button, Heading, VStack, Divider } from '@chakra-ui/react';
import React from 'react';
import { Divider } from '@chakra-ui/react';
import i18next from 'i18next';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
const Sidebar = () => (
<Box
borderRight='1px solid black'
bg='gray.50'
color='white'
w='250px'
p='5'
pt='8'
>
<Heading color='green' size='lg' mb='5'>
{i18next.t(`dry-wash.arm.master.sideBar.title`)}
</Heading>
import { URLs } from '../../__data__/urls';
<VStack align='start' spacing='4'>
<Divider />
<Button
as={Link}
to='orders'
w='100%'
colorScheme='green'
variant='ghost'
>
{i18next.t(`dry-wash.arm.master.sideBar.title.orders`)}
</Button>
<Divider />
<Button
as={Link}
to='masters'
w='100%'
colorScheme='green'
variant='ghost'
>
{i18next.t(`dry-wash.arm.master.sideBar.title.master`)}
</Button>
<Divider />
</VStack>
</Box>
);
const Sidebar = () => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master.sideBar',
});
return (
<Box
borderRight='1px solid black'
bg='gray.50'
color='white'
w='250px'
p='5'
pt='8'
>
<Heading color='green' size='lg' mb='5'>
{t('title')}
</Heading>
<VStack align='start' spacing='4'>
<Divider />
{URLs.armOrder.isOn && (
<Button
as={Link}
to={URLs.armOrder.url}
w='100%'
colorScheme='green'
variant='ghost'
>
{t('orders')}
</Button>
)}
<Divider />
{URLs.armMaster.isOn && (
<Button
as={Link}
to={URLs.armMaster.url}
w='100%'
colorScheme='green'
variant='ghost'
>
{t('master')}
</Button>
)}
<Divider />
</VStack>
</Box>
);
};
export default Sidebar;

View File

@@ -1,29 +1,52 @@
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import {
MdEco,
MdMiscellaneousServices,
MdPlace,
MdHandshake,
} from 'react-icons/md';
import { Heading, HStack, List, Text, VStack } from '@chakra-ui/react';
import { CtaButton, PageSection } from '../';
import { ListItem } from './ListItem';
import { BenefitsSectionProps } from './types';
import { iconsMap } from './helper';
export const BenefitsSection: FC<BenefitsSectionProps> = ({
data: { heading, description, list } = {},
}) => {
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
export const BenefitsSection: FC = () => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.landing.benefits-section',
});
const listData = [
{
Icon: MdEco,
children: t('list.0'),
},
{
Icon: MdMiscellaneousServices,
children: t('list.1'),
},
{
Icon: MdPlace,
children: t('list.2'),
},
{
Icon: MdHandshake,
children: t('list.3'),
},
];
return (
<PageSection>
<VStack w='full' spacing={2}>
<Heading as='h2'>{t(heading)}</Heading>
<Text>{t(description)}</Text>
<Heading as='h2'>{t('heading')}</Heading>
<Text>
{t('description')}
</Text>
</VStack>
<List display='flex' flexDirection='column' spacing={3}>
{list.map((itemKey, i) => (
<ListItem key={i} Icon={iconsMap[itemKey]}>
{t(itemKey)}
</ListItem>
{listData.map((props, i) => (
<ListItem key={i} {...props} />
))}
</List>
<HStack w='full' justify='flex-end'>

View File

@@ -1,13 +0,0 @@
import { IconType } from "react-icons";
import { MdEco, MdMiscellaneousServices, MdPlace, MdHandshake } from "react-icons/md";
import { ArrElement } from "../../../lib";
import { BenefitsList } from "./types";
export const iconsMap: Record<ArrElement<BenefitsList>, IconType> = {
"benefits-section.list.0": MdEco,
"benefits-section.list.1": MdMiscellaneousServices,
"benefits-section.list.2": MdPlace,
"benefits-section.list.3": MdHandshake,
};

View File

@@ -1,2 +1 @@
export type { BenefitsSectionProps } from './types';
export { BenefitsSection } from './BenefitsSection';

View File

@@ -1,14 +0,0 @@
export type BenefitsList = [
'benefits-section.list.0',
'benefits-section.list.1',
'benefits-section.list.2',
'benefits-section.list.3',
];
export type BenefitsSectionProps = {
data: {
heading: 'benefits-section.heading';
description: 'benefits-section.description';
list: BenefitsList;
};
};

View File

@@ -6,7 +6,7 @@ import { ButtonProps, Button } from '@chakra-ui/react';
import { URLs } from '../../../__data__/urls';
export const CtaButton: FC<ButtonProps> = (props) => {
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
const { t } = useTranslation();
return (
<Button
@@ -15,7 +15,7 @@ export const CtaButton: FC<ButtonProps> = (props) => {
colorScheme='primary'
{...props}
>
{t('make-order-button')}
{t('~:dry-wash.landing.make-order-button')}
</Button>
);
};

View File

@@ -1,23 +1,22 @@
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Heading, Text, Center, VStack } from '@chakra-ui/react';
import { Box, Heading, Text, Center, VStack, BoxProps } from '@chakra-ui/react';
import { DemoVideoPosterImg } from '../../../assets/images';
import { CtaButton, SiteLogo, PageSection } from '../';
import { HeroSectionProps } from './types';
type HeroSectionProps = Pick<BoxProps, 'flexShrink'>;
export const HeroSection: FC<HeroSectionProps> = ({
data: { headline, description, video } = {},
flexShrink,
}) => {
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
export const HeroSection: FC<HeroSectionProps> = ({ flexShrink }) => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.landing.hero-section',
});
return (
<Box flexShrink={flexShrink} as='header' pos='relative' zIndex={0}>
<Box
as='video'
src={`${__webpack_public_path__}/remote-assets/${video}`}
src={`${__webpack_public_path__}/remote-assets/demo.mp4`}
poster={DemoVideoPosterImg}
autoPlay
loop
@@ -48,14 +47,14 @@ export const HeroSection: FC<HeroSectionProps> = ({
color='white'
__css={{ textWrap: 'balance' }}
>
{t(headline)}
{t('headline')}
</Heading>
<Text
textAlign='center'
__css={{ textWrap: 'balance' }}
color='white'
>
{t(description)}
{t('description')}
</Text>
</VStack>
<CtaButton size='lg' />

View File

@@ -1,9 +0,0 @@
import { BoxProps } from "@chakra-ui/react";
export type HeroSectionProps = {
data: {
headline: 'hero-section.headline';
description: 'hero-section.description';
video: string;
};
} & Pick<BoxProps, 'flexShrink'>;

View File

@@ -7,7 +7,7 @@ import { LogoSvg } from '../../../assets/icons';
export const SiteLogo: FC = () => {
const { t } = useTranslation();
return <Image src={LogoSvg} alt={t('dry-wash.landing.site-logo')} w={40} />;
return <Image src={LogoSvg} alt={t('~:dry-wash.landing.site-logo')} w={40} />;
};
// todo: replace Image by SVG React component

View File

@@ -5,16 +5,15 @@ import { Heading, HStack } from '@chakra-ui/react';
import { CtaButton, PageSection } from '../';
import { ReviewsSlider } from './ReviewsSlider';
import { SocialProofSectionProps } from './types';
export const SocialProofSection: FC<SocialProofSectionProps> = ({
data: { heading } = {},
}) => {
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
export const SocialProofSection: FC = () => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.landing.social-proof-section',
});
return (
<PageSection>
<Heading as='h2'>{t(heading)}</Heading>
<Heading as='h2'>{t('heading')}</Heading>
<ReviewsSlider />
<HStack w='full' justify='flex-end'>
<CtaButton />

View File

@@ -1,2 +1 @@
export type { SocialProofSectionProps } from './types';
export { SocialProofSection } from './SocialProofSection';

View File

@@ -1,5 +0,0 @@
export type SocialProofSectionProps = {
data: {
heading: 'social-proof-section.heading';
};
};

View File

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

View File

@@ -1,41 +0,0 @@
/**
* @example type Output = ArrElement<['a', 'b', 'c']>;
* // "a" | "b" | "c"
*/
export type ArrElement<ArrType> = ArrType extends readonly (infer ElementType)[]
? ElementType
: never;
/**
* @example type Output = Split<'a.b1' | 'a.b2', '.'>;
* // ["a", "b1"] | ["a", "b2"]
*/
type Split<S extends string, D extends string> =
S extends `${infer A}${D}${infer B}` ? [A, ...Split<B, D>] : [S];
/**
* @example type Output = NestedObject<["a", "b1"] | ["a", "b2"]>;
* // { a: { b1: string; }; } | { a: { b2: string; }; }
*/
type NestedObject<T extends string[]> =
T extends [infer Head, ...infer Tail] ?
Head extends string ?
{ [key in Head]: NestedObject<Tail extends string[] ? Tail : []> } : never :
string;
/**
* @example type Output = UnionToIntersection<{ a: { b1: string; }; } | { a: { b2: string; }; }>;
* // { a: { b1: string; }; } & { a: { b2: string; }; }
*/
type UnionToIntersection<U> =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
/**
* @example type Output = CreateTree<'a.b1' | 'a.b2', '.'>;
* // { a: { b1: string; }; } & { a: { b2: string; }; }
*/
export type CreateTree<T> =
UnionToIntersection<T extends infer U ?
U extends string ?
NestedObject<Split<U, '.'>> : never : never>;

View File

@@ -1,5 +1,24 @@
import defaultLocale from '../../locales/ru.json';
import { CreateTree } from '../lib';
type Split<S extends string, D extends string> =
S extends `${infer A}${D}${infer B}` ? [A, ...Split<B, D>] : [S];
type NestedObject<T extends string[]> =
T extends [infer Head, ...infer Tail] ?
Head extends string ?
{ [key in Head]: NestedObject<Tail extends string[] ? Tail : []> } : never :
string;
// Основная утилита для обработки union type
type CreateTree<T> =
UnionToIntersection<T extends infer U ?
U extends string ?
NestedObject<Split<U, '.'>> : never : never>;
// Утилита для объединения типов
type UnionToIntersection<U> =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type LanguageResource = CreateTree<keyof typeof defaultLocale>;
@@ -7,6 +26,6 @@ declare module "i18next" {
interface CustomTypeOptions {
resources: {
'~': LanguageResource
}
};
}
}

View File

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

View File

@@ -1 +0,0 @@
export { default as LandingSuccessStub } from './success';

View File

@@ -1,27 +0,0 @@
// Generated by json-literal-typer
// <-- BEGIN
interface HeroXsection {
description: "hero-section.description";
headline: "hero-section.headline";
video: "demo.mp4";
}
interface Sections {
description?: "benefits-section.description";
heading: "benefits-section.heading" | "social-proof-section.heading";
list?: ("benefits-section.list.0" | "benefits-section.list.1" | "benefits-section.list.2" | "benefits-section.list.3")[];
type: "benefits-section" | "social-proof-section";
}
interface Body {
"hero-section": HeroXsection;
sections: Sections[];
}
interface Root {
body: Body;
success: true;
}
// END -->
export default Root;

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import LayoutArm from '../../components/LayoutArm';

View File

@@ -8,12 +8,6 @@ import {
SocialProofSection,
} from '../../components/landing';
import { LandingThemeProvider } from '../../containers';
import { LandingSuccessStub } from '../../models/landing';
const landingSuccessStub = import(
'../../../stubs/json/landing/success.json'
) as unknown as LandingSuccessStub;
import { isBenefitsSectionData, isSocialProofSectionData } from './types';
const Page: FC = () => {
return (
@@ -27,19 +21,10 @@ const Page: FC = () => {
centerContent
>
<VStack w='full' h='full' alignItems='stretch' flexGrow={1}>
<HeroSection
data={landingSuccessStub['body']['hero-section']}
flexShrink={0}
/>
<HeroSection flexShrink={0} />
<VStack as='main' flexGrow={1}>
{landingSuccessStub.body.sections.map(({ type, ...data }, i) => {
if (isBenefitsSectionData(type, data)) {
return <BenefitsSection key={i} data={data} />;
}
if (isSocialProofSectionData(type, data)) {
return <SocialProofSection key={i} data={data} />;
}
})}
<BenefitsSection />
<SocialProofSection />
</VStack>
<Footer />
</VStack>

View File

@@ -1,15 +0,0 @@
import LandingSuccess from "../../../stubs/json/landing/success.json";
import { BenefitsSectionProps, SocialProofSectionProps } from "../../components/landing";
import { ArrElement } from "../../lib";
type SectionsItemData = ArrElement<typeof LandingSuccess['body']['sections']>;
type SectionType = SectionsItemData['type'];
type SectionData = Omit<SectionsItemData, 'type'>;
export const isBenefitsSectionData = (type: SectionType, data: SectionData): data is BenefitsSectionProps['data'] => {
return type === 'benefits-section';
};
export const isSocialProofSectionData = (type: SectionType, data: SectionData): data is SocialProofSectionProps['data'] => {
return type === 'social-proof-section';
};

View File

@@ -17,7 +17,9 @@ const Routers = () => {
<Route path={URLs.landing.url} element={<Landing />} />
<Route path={URLs.orderForm.url} element={<OrderForm />} />
<Route path={URLs.orderView.url} element={<OrderView />} />
<Route path='/dry-wash/arm/*' element={<Arm />}></Route>
{URLs.armBase.isOn && (
<Route path={URLs.armBase.url} element={<Arm />}></Route>
)}
<Route path='*' element={<NotFound />} />
</Routes>
</Suspense>

View File

@@ -1,27 +0,0 @@
{
"success": true,
"body": {
"hero-section": {
"headline": "hero-section.headline",
"description": "hero-section.description",
"video": "demo.mp4"
},
"sections": [
{
"type": "benefits-section",
"heading": "benefits-section.heading",
"description": "benefits-section.description",
"list": [
"benefits-section.list.0",
"benefits-section.list.1",
"benefits-section.list.2",
"benefits-section.list.3"
]
},
{
"type": "social-proof-section",
"heading": "social-proof-section.heading"
}
]
}
}