Compare commits

..

No commits in common. "52bb3790c7752afb02a257be36e827aa456a1c8a" and "7025c1a31bfb69d82a1e6b3d43c228078c276fbe" have entirely different histories.

21 changed files with 90 additions and 218 deletions

View File

@ -45,12 +45,10 @@
### MVP1 ### MVP1
**1. Landing** **1. Landing**
- преимущества сервиса - преимущества сервиса
- оставить заявку (редирект на Страницу оформления заказа) - оставить заявку (редирект на Страницу оформления заказа)
**2. Страница для оформления заказа** **2. Страница для оформления заказа**
- форма - форма
- номер машины (mask input) - номер машины (mask input)
- цвет машины - цвет машины
@ -60,12 +58,10 @@
- после заполнения редирект на страницу с деталями заказа - после заполнения редирект на страницу с деталями заказа
**3. Страница с деталями заказа** **3. Страница с деталями заказа**
- описание заказа - описание заказа
- детали заказа (id, статус) - детали заказа (id, статус)
**3. АРМ оператора** **3. АРМ оператора**
- список заказов (RUD) - список заказов (RUD)
- id заказа - id заказа
- статус заказа (готово / не готово) - статус заказа (готово / не готово)
@ -76,6 +72,7 @@
- кнопка "Добавить" - кнопка "Добавить"
- кнопка "Удалить" - кнопка "Удалить"
### Built With ### Built With
[![React][React.js]][React-url] [![React][React.js]][React-url]
@ -106,14 +103,6 @@
<p align="right">(<a href="#readme-top">back to top</a>)</p> <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 -->
## Participants ## Participants

14
package-lock.json generated
View File

@ -6964,6 +6964,20 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC" "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": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",

View File

@ -1,29 +1,52 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { useTranslation } from 'react-i18next'; 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 { Heading, HStack, List, Text, VStack } from '@chakra-ui/react';
import { CtaButton, PageSection } from '../'; import { CtaButton, PageSection } from '../';
import { ListItem } from './ListItem'; import { ListItem } from './ListItem';
import { BenefitsSectionProps } from './types';
import { iconsMap } from './helper';
export const BenefitsSection: FC<BenefitsSectionProps> = ({ export const BenefitsSection: FC = () => {
data: { heading, description, list } = {}, const { t } = useTranslation('~', {
}) => { keyPrefix: 'dry-wash.landing.benefits-section',
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' }); });
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 ( return (
<PageSection> <PageSection>
<VStack w='full' spacing={2}> <VStack w='full' spacing={2}>
<Heading as='h2'>{t(heading)}</Heading> <Heading as='h2'>{t('heading')}</Heading>
<Text>{t(description)}</Text> <Text>
{t('description')}
</Text>
</VStack> </VStack>
<List display='flex' flexDirection='column' spacing={3}> <List display='flex' flexDirection='column' spacing={3}>
{list.map((itemKey, i) => ( {listData.map((props, i) => (
<ListItem key={i} Icon={iconsMap[itemKey]}> <ListItem key={i} {...props} />
{t(itemKey)}
</ListItem>
))} ))}
</List> </List>
<HStack w='full' justify='flex-end'> <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'; 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'; import { URLs } from '../../../__data__/urls';
export const CtaButton: FC<ButtonProps> = (props) => { export const CtaButton: FC<ButtonProps> = (props) => {
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' }); const { t } = useTranslation();
return ( return (
<Button <Button
@ -15,7 +15,7 @@ export const CtaButton: FC<ButtonProps> = (props) => {
colorScheme='primary' colorScheme='primary'
{...props} {...props}
> >
{t('make-order-button')} {t('~:dry-wash.landing.make-order-button')}
</Button> </Button>
); );
}; };

View File

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

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

View File

@ -1,2 +1 @@
export type { SocialProofSectionProps } from './types';
export { SocialProofSection } from './SocialProofSection'; 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 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>; type LanguageResource = CreateTree<keyof typeof defaultLocale>;
@ -7,6 +26,6 @@ declare module "i18next" {
interface CustomTypeOptions { interface CustomTypeOptions {
resources: { resources: {
'~': LanguageResource '~': 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

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

@ -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"
}
]
}
}