Compare commits
27 Commits
6442f1f407
...
v0.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6705e74ece | ||
| 52bb3790c7 | |||
|
|
2cf60c7b16 | ||
|
|
d30202108c | ||
|
|
012de3c670 | ||
|
|
409c339b3c | ||
|
|
8611405edd | ||
|
|
407da721af | ||
|
|
409473413a | ||
|
|
823e192c51 | ||
|
|
d4965b7aba | ||
|
|
16b4627166 | ||
| 7025c1a31b | |||
| 2d55f26de5 | |||
| da244adee0 | |||
| 5d1a679c68 | |||
| a9738c7d8a | |||
| 2130532dfd | |||
| 3b4f5a8486 | |||
| acd39b1e32 | |||
| 9eb0080d13 | |||
| 920f4440e9 | |||
| e374cbfdd3 | |||
|
|
181198c0cb | ||
|
|
5ca3fd2613 | ||
|
|
0cd7ec8b22 | ||
|
|
66c1323f00 |
13
README.md
13
README.md
@@ -45,10 +45,12 @@
|
||||
### MVP1
|
||||
|
||||
**1. Landing**
|
||||
|
||||
- преимущества сервиса
|
||||
- оставить заявку (редирект на Страницу оформления заказа)
|
||||
|
||||
**2. Страница для оформления заказа**
|
||||
|
||||
- форма
|
||||
- номер машины (mask input)
|
||||
- цвет машины
|
||||
@@ -58,10 +60,12 @@
|
||||
- после заполнения редирект на страницу с деталями заказа
|
||||
|
||||
**3. Страница с деталями заказа**
|
||||
|
||||
- описание заказа
|
||||
- детали заказа (id, статус)
|
||||
|
||||
**3. АРМ оператора**
|
||||
|
||||
- список заказов (RUD)
|
||||
- id заказа
|
||||
- статус заказа (готово / не готово)
|
||||
@@ -72,7 +76,6 @@
|
||||
- кнопка "Добавить"
|
||||
- кнопка "Удалить"
|
||||
|
||||
|
||||
### Built With
|
||||
|
||||
[![React][React.js]][React-url]
|
||||
@@ -103,6 +106,14 @@
|
||||
|
||||
<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
|
||||
|
||||
@@ -14,8 +14,8 @@ module.exports = {
|
||||
'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.master': 'master',
|
||||
'dry-wash.arm.order': 'order',
|
||||
'dry-wash.arm': '/arm/*',
|
||||
},
|
||||
features: {
|
||||
|
||||
@@ -49,5 +49,6 @@
|
||||
"dry-wash.notFound.button.back": "Вернуться на главную",
|
||||
"dry-wash.errorBoundary.title":"Что-то пошло не так",
|
||||
"dry-wash.errorBoundary.description": "Мы уже работаем над исправлением проблемы",
|
||||
"dry-wash.errorBoundary.button.reload": "Перезагрузить страницу"
|
||||
"dry-wash.errorBoundary.button.reload": "Перезагрузить страницу",
|
||||
"dry-wash.washTime.timeSlot": "{{start}} - {{end}}"
|
||||
}
|
||||
|
||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dry-wash",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dry-wash",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@brojs/cli": "^1.6.3",
|
||||
@@ -17,6 +17,7 @@
|
||||
"@fontsource/open-sans": "^5.1.0",
|
||||
"@lottiefiles/react-lottie-player": "^3.5.4",
|
||||
"@types/react": "^18.3.12",
|
||||
"dayjs": "^1.11.13",
|
||||
"express": "^4.21.1",
|
||||
"framer-motion": "^6.2.8",
|
||||
"i18next": "^23.16.4",
|
||||
@@ -5304,6 +5305,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
@@ -6958,20 +6964,6 @@
|
||||
"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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dry-wash",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "<a id=\"readme-top\"></a>",
|
||||
"main": "./src/index.tsx",
|
||||
"scripts": {
|
||||
@@ -25,6 +25,7 @@
|
||||
"@fontsource/open-sans": "^5.1.0",
|
||||
"@lottiefiles/react-lottie-player": "^3.5.4",
|
||||
"@types/react": "^18.3.12",
|
||||
"dayjs": "^1.11.13",
|
||||
"express": "^4.21.1",
|
||||
"framer-motion": "^6.2.8",
|
||||
"i18next": "^23.16.4",
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Badge, Link, Stack, Td, Tr } from '@chakra-ui/react';
|
||||
|
||||
import MasterActionsMenu from '../MasterActionsMenu';
|
||||
import { getTimeSlot } from '../../lib/date-helpers';
|
||||
|
||||
const MasterItem = ({ name, schedule, phone }) => {
|
||||
return (
|
||||
@@ -9,9 +10,9 @@ const MasterItem = ({ name, schedule, phone }) => {
|
||||
<Td>{name}</Td>
|
||||
<Td>
|
||||
<Stack direction='row'>
|
||||
{schedule.map((time, index) => (
|
||||
{schedule.map(({ startWashTime, endWashTime }, index) => (
|
||||
<Badge colorScheme={'green'} key={index}>
|
||||
{time}
|
||||
{getTimeSlot(startWashTime, endWashTime)}
|
||||
</Badge>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { mastersData } from '../../mocks';
|
||||
import MasterItem from '../MasterItem';
|
||||
import MasterDrawer from '../MasterDrawer';
|
||||
import data from '../../../stubs/json/arm-masters/success.json';
|
||||
|
||||
const TABLE_HEADERS = [
|
||||
'name' as const,
|
||||
@@ -48,7 +48,7 @@ const Masters = () => {
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{mastersData.map((master, index) => (
|
||||
{data.body.map((master, index) => (
|
||||
<MasterItem key={index} {...master} />
|
||||
))}
|
||||
</Tbody>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Td, Tr, Link, Select } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { getTimeSlot } from '../../lib/date-helpers';
|
||||
|
||||
const statuses = [
|
||||
'pending' as const,
|
||||
@@ -15,7 +18,8 @@ type GetArrItemType<ArrType> =
|
||||
|
||||
export type OrderProps = {
|
||||
carNumber?: string;
|
||||
washTime?: string;
|
||||
startWashTime?: string;
|
||||
endWashTime?: string;
|
||||
orderDate?: string;
|
||||
status?: GetArrItemType<typeof statuses>;
|
||||
phone?: string;
|
||||
@@ -24,7 +28,8 @@ export type OrderProps = {
|
||||
|
||||
const OrderItem = ({
|
||||
carNumber,
|
||||
washTime,
|
||||
startWashTime,
|
||||
endWashTime,
|
||||
orderDate,
|
||||
status,
|
||||
phone,
|
||||
@@ -39,8 +44,8 @@ const OrderItem = ({
|
||||
return (
|
||||
<Tr>
|
||||
<Td>{carNumber}</Td>
|
||||
<Td>{washTime}</Td>
|
||||
<Td>{orderDate}</Td>
|
||||
<Td>{getTimeSlot(startWashTime, endWashTime)}</Td>
|
||||
<Td>{dayjs(orderDate).format('DD.MM.YYYY')}</Td>
|
||||
<Td>
|
||||
<Select
|
||||
value={statusSelect}
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from 'react';
|
||||
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';
|
||||
import data from '../../../stubs/json/arm-orders/success.json';
|
||||
|
||||
const Orders = () => {
|
||||
const { t } = useTranslation('~', {
|
||||
@@ -34,7 +34,7 @@ const Orders = () => {
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{ordersData.map((order, index) => (
|
||||
{data.body.map((order, index) => (
|
||||
<OrderItem
|
||||
key={index}
|
||||
{...order}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Box, Button, Heading, VStack, Divider } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useLocation, Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { URLs } from '../../__data__/urls';
|
||||
|
||||
const Sidebar = () => {
|
||||
const location = useLocation();
|
||||
const isActive = (keyword: string) => location.pathname.includes(keyword);
|
||||
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.arm.master.sideBar',
|
||||
});
|
||||
@@ -30,8 +33,8 @@ const Sidebar = () => {
|
||||
as={Link}
|
||||
to={URLs.armOrder.url}
|
||||
w='100%'
|
||||
colorScheme='green'
|
||||
variant='ghost'
|
||||
colorScheme={isActive(URLs.armOrder.url) ? 'green' : 'blue'}
|
||||
variant={isActive(URLs.armOrder.url) ? 'solid' : 'ghost'}
|
||||
>
|
||||
{t('orders')}
|
||||
</Button>
|
||||
@@ -42,8 +45,8 @@ const Sidebar = () => {
|
||||
as={Link}
|
||||
to={URLs.armMaster.url}
|
||||
w='100%'
|
||||
colorScheme='green'
|
||||
variant='ghost'
|
||||
colorScheme={isActive(URLs.armMaster.url) ? 'green' : 'blue'}
|
||||
variant={isActive(URLs.armMaster.url) ? 'solid' : 'ghost'}
|
||||
>
|
||||
{t('master')}
|
||||
</Button>
|
||||
|
||||
@@ -1,52 +1,29 @@
|
||||
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 = () => {
|
||||
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'),
|
||||
},
|
||||
];
|
||||
export const BenefitsSection: FC<BenefitsSectionProps> = ({
|
||||
data: { heading, description, list } = {},
|
||||
}) => {
|
||||
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
|
||||
|
||||
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}>
|
||||
{listData.map((props, i) => (
|
||||
<ListItem key={i} {...props} />
|
||||
{list.map((itemKey, i) => (
|
||||
<ListItem key={i} Icon={iconsMap[itemKey]}>
|
||||
{t(itemKey)}
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<HStack w='full' justify='flex-end'>
|
||||
|
||||
13
src/components/landing/BenefitsSection/helper.ts
Normal file
13
src/components/landing/BenefitsSection/helper.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
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,
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export type { BenefitsSectionProps } from './types';
|
||||
export { BenefitsSection } from './BenefitsSection';
|
||||
14
src/components/landing/BenefitsSection/types.ts
Normal file
14
src/components/landing/BenefitsSection/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
};
|
||||
};
|
||||
@@ -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();
|
||||
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -15,7 +15,7 @@ export const CtaButton: FC<ButtonProps> = (props) => {
|
||||
colorScheme='primary'
|
||||
{...props}
|
||||
>
|
||||
{t('~:dry-wash.landing.make-order-button')}
|
||||
{t('make-order-button')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import React, { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Heading, Text, Center, VStack, BoxProps } from '@chakra-ui/react';
|
||||
import { Box, Heading, Text, Center, VStack } from '@chakra-ui/react';
|
||||
|
||||
import { DemoVideoPosterImg } from '../../../assets/images';
|
||||
import { CtaButton, SiteLogo, PageSection } from '../';
|
||||
|
||||
type HeroSectionProps = Pick<BoxProps, 'flexShrink'>;
|
||||
import { HeroSectionProps } from './types';
|
||||
|
||||
export const HeroSection: FC<HeroSectionProps> = ({ flexShrink }) => {
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.landing.hero-section',
|
||||
});
|
||||
export const HeroSection: FC<HeroSectionProps> = ({
|
||||
data: { headline, description, video } = {},
|
||||
flexShrink,
|
||||
}) => {
|
||||
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
|
||||
|
||||
return (
|
||||
<Box flexShrink={flexShrink} as='header' pos='relative' zIndex={0}>
|
||||
<Box
|
||||
as='video'
|
||||
src={`${__webpack_public_path__}/remote-assets/demo.mp4`}
|
||||
src={`${__webpack_public_path__}/remote-assets/${video}`}
|
||||
poster={DemoVideoPosterImg}
|
||||
autoPlay
|
||||
loop
|
||||
@@ -47,14 +48,14 @@ export const HeroSection: FC<HeroSectionProps> = ({ flexShrink }) => {
|
||||
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' />
|
||||
|
||||
9
src/components/landing/HeroSection/types.ts
Normal file
9
src/components/landing/HeroSection/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { BoxProps } from "@chakra-ui/react";
|
||||
|
||||
export type HeroSectionProps = {
|
||||
data: {
|
||||
headline: 'hero-section.headline';
|
||||
description: 'hero-section.description';
|
||||
video: string;
|
||||
};
|
||||
} & Pick<BoxProps, 'flexShrink'>;
|
||||
@@ -5,15 +5,16 @@ import { Heading, HStack } from '@chakra-ui/react';
|
||||
import { CtaButton, PageSection } from '../';
|
||||
|
||||
import { ReviewsSlider } from './ReviewsSlider';
|
||||
import { SocialProofSectionProps } from './types';
|
||||
|
||||
export const SocialProofSection: FC = () => {
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.landing.social-proof-section',
|
||||
});
|
||||
export const SocialProofSection: FC<SocialProofSectionProps> = ({
|
||||
data: { heading } = {},
|
||||
}) => {
|
||||
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
<Heading as='h2'>{t('heading')}</Heading>
|
||||
<Heading as='h2'>{t(heading)}</Heading>
|
||||
<ReviewsSlider />
|
||||
<HStack w='full' justify='flex-end'>
|
||||
<CtaButton />
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export type { SocialProofSectionProps } from './types';
|
||||
export { SocialProofSection } from './SocialProofSection';
|
||||
5
src/components/landing/SocialProofSection/types.ts
Normal file
5
src/components/landing/SocialProofSection/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type SocialProofSectionProps = {
|
||||
data: {
|
||||
heading: 'social-proof-section.heading';
|
||||
};
|
||||
};
|
||||
12
src/lib/date-helpers.ts
Normal file
12
src/lib/date-helpers.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import dayjs from 'dayjs';
|
||||
import i18next from 'i18next';
|
||||
|
||||
export const getTimeSlot = (start: string, end: string) => {
|
||||
const startDay = dayjs(start).format('HH:mm');
|
||||
const endDay = dayjs(end).format('HH:mm');
|
||||
|
||||
return i18next.t('~:dry-wash.washTime.timeSlot', {
|
||||
start: startDay,
|
||||
end: endDay,
|
||||
});
|
||||
};
|
||||
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './types';
|
||||
41
src/lib/types.ts
Normal file
41
src/lib/types.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @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>;
|
||||
@@ -1,35 +0,0 @@
|
||||
export const mastersData = [
|
||||
{
|
||||
id: 'masters1',
|
||||
name: 'Иван Иванов',
|
||||
schedule: ['15:00 - 16:30', '17:00 - 18:00'],
|
||||
phone: '+7 900 123 45 67',
|
||||
},
|
||||
{
|
||||
id: 'masters2',
|
||||
name: 'Сергей Петров',
|
||||
schedule: ['10:00 - 12:30', '14:00 - 15:30', '16:00 - 17:00'],
|
||||
phone: '+7 900 234 56 78',
|
||||
},
|
||||
];
|
||||
|
||||
export const ordersData = [
|
||||
{
|
||||
id: 'order1',
|
||||
carNumber: 'A123BC',
|
||||
washTime: '10:30',
|
||||
orderDate: '2024-10-31',
|
||||
status: 'progress',
|
||||
phone: '79001234567',
|
||||
location: 'Казань, ул. Баумана, 1',
|
||||
},
|
||||
{
|
||||
id: 'order2',
|
||||
carNumber: 'B456CD',
|
||||
washTime: '11:00',
|
||||
orderDate: '2024-10-31',
|
||||
status: 'complete',
|
||||
phone: '79002345678',
|
||||
location: 'Казань, ул. Кремлёвская, 3',
|
||||
},
|
||||
];
|
||||
23
src/models/i18next.d.ts
vendored
23
src/models/i18next.d.ts
vendored
@@ -1,24 +1,5 @@
|
||||
import defaultLocale from '../../locales/ru.json';
|
||||
|
||||
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;
|
||||
import { CreateTree } from '../lib';
|
||||
|
||||
type LanguageResource = CreateTree<keyof typeof defaultLocale>;
|
||||
|
||||
@@ -26,6 +7,6 @@ declare module "i18next" {
|
||||
interface CustomTypeOptions {
|
||||
resources: {
|
||||
'~': LanguageResource
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/models/landing/index.ts
Normal file
1
src/models/landing/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './stubs';
|
||||
1
src/models/landing/stubs/index.ts
Normal file
1
src/models/landing/stubs/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as LandingSuccessStub } from './success';
|
||||
27
src/models/landing/stubs/success.ts
Normal file
27
src/models/landing/stubs/success.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// 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;
|
||||
@@ -8,6 +8,12 @@ import {
|
||||
SocialProofSection,
|
||||
} from '../../components/landing';
|
||||
import { LandingThemeProvider } from '../../containers';
|
||||
import { LandingSuccessStub } from '../../models/landing';
|
||||
import landingSuccessStubJson from '../../../stubs/json/landing/success.json';
|
||||
|
||||
import { isBenefitsSectionData, isSocialProofSectionData } from './types';
|
||||
|
||||
const landingSuccessStub = landingSuccessStubJson as LandingSuccessStub;
|
||||
|
||||
const Page: FC = () => {
|
||||
return (
|
||||
@@ -21,10 +27,19 @@ const Page: FC = () => {
|
||||
centerContent
|
||||
>
|
||||
<VStack w='full' h='full' alignItems='stretch' flexGrow={1}>
|
||||
<HeroSection flexShrink={0} />
|
||||
<HeroSection
|
||||
data={landingSuccessStub['body']['hero-section']}
|
||||
flexShrink={0}
|
||||
/>
|
||||
<VStack as='main' flexGrow={1}>
|
||||
<BenefitsSection />
|
||||
<SocialProofSection />
|
||||
{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} />;
|
||||
}
|
||||
})}
|
||||
</VStack>
|
||||
<Footer />
|
||||
</VStack>
|
||||
|
||||
15
src/pages/landing/types.ts
Normal file
15
src/pages/landing/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
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';
|
||||
};
|
||||
35
stubs/json/arm-masters/success.json
Normal file
35
stubs/json/arm-masters/success.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"success": true,
|
||||
"body": [
|
||||
{
|
||||
"id": "masters1",
|
||||
"name": "Иван Иванов",
|
||||
"schedule": [ {
|
||||
"id": "order1",
|
||||
"startWashTime": "2024-11-24T10:30:00.000Z",
|
||||
"endWashTime": "2024-11-24T16:30:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "order2",
|
||||
"startWashTime": "2024-11-24T11:30:00.000Z",
|
||||
"endWashTime": "2024-11-24T17:30:00.000Z"
|
||||
}],
|
||||
"phone": "+7 900 123 45 67"
|
||||
},
|
||||
{
|
||||
"id": "masters12",
|
||||
"name": "Иван Иванов",
|
||||
"schedule": [ {
|
||||
"id": "order1",
|
||||
"startWashTime": "2024-11-24T10:30:00.000Z",
|
||||
"endWashTime": "2024-11-24T16:30:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "order2",
|
||||
"startWashTime": "2024-11-24T11:30:00.000Z",
|
||||
"endWashTime": "2024-11-24T17:30:00.000Z"
|
||||
}],
|
||||
"phone": "+7 900 123 45 67"
|
||||
}
|
||||
]
|
||||
}
|
||||
25
stubs/json/arm-orders/success.json
Normal file
25
stubs/json/arm-orders/success.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"success": true,
|
||||
"body": [
|
||||
{
|
||||
"id": "order1",
|
||||
"carNumber": "A123BC",
|
||||
"startWashTime": "2024-11-24T10:30:00.000Z",
|
||||
"endWashTime": "2024-11-24T16:30:00.000Z",
|
||||
"orderDate": "2024-11-24T08:41:46.366Z",
|
||||
"status": "progress",
|
||||
"phone": "79001234563",
|
||||
"location": "Казань, ул. Баумана, 1"
|
||||
},
|
||||
{
|
||||
"id": "order2",
|
||||
"carNumber": "A245BC",
|
||||
"startWashTime": "2024-11-24T11:30:00.000Z",
|
||||
"endWashTime": "2024-11-24T17:30:00.000Z",
|
||||
"orderDate": "2024-11-24T07:40:46.366Z",
|
||||
"status": "progress",
|
||||
"phone": "79001234567",
|
||||
"location": "Казань, ул. Баумана, 43"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
stubs/json/landing/success.json
Normal file
27
stubs/json/landing/success.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user