feat/landing #30

Merged
primakov merged 17 commits from feat/landing into main 2024-11-17 11:39:27 +03:00
26 changed files with 751 additions and 183 deletions

17
locales/en.json Normal file
View File

@ -0,0 +1,17 @@
{
"dry-wash.landing.benefits-section.description": "Discover the benefits of our car dry cleaning services",
"dry-wash.landing.benefits-section.heading": "Advantages of an eco-friendly car wash",
"dry-wash.landing.benefits-section.list.0": "Environmentally friendly products",
"dry-wash.landing.benefits-section.list.1": "Fast and efficient service",
"dry-wash.landing.benefits-section.list.2": "Convenient mobile access",
"dry-wash.landing.benefits-section.list.3": "Reliable and trustworthy",
"dry-wash.landing.footer.copyright": "\u00A9 {{currentYear}} DryMaster. All rights reserved",
"dry-wash.landing.footer.links.faq": "FAQ",
"dry-wash.landing.footer.links.privacy-policy": "Privacy policy",
"dry-wash.landing.footer.links.service-terms": "Terms of service",
"dry-wash.landing.hero-section.description": "Experience the ultimate convenience of a dry car wash, designed for a cleaner planet.",
"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"
}

View File

@ -29,6 +29,21 @@
"dry-wash.arm.master.sideBar.title": " Сухой мастер", "dry-wash.arm.master.sideBar.title": " Сухой мастер",
"dry-wash.arm.master.sideBar.title.master": "Мастера", "dry-wash.arm.master.sideBar.title.master": "Мастера",
"dry-wash.arm.master.sideBar.title.orders": "Заказы", "dry-wash.arm.master.sideBar.title.orders": "Заказы",
"dry-wash.landing.benefits-section.description": "Откройте для себя преимущества наших услуг по химчистке автомобилей",
"dry-wash.landing.benefits-section.heading": "Преимущества экологичной автомойки",
"dry-wash.landing.benefits-section.list.0": "Экологически безопасные продукты",
"dry-wash.landing.benefits-section.list.1": "Быстрое и эффективное обслуживание",
"dry-wash.landing.benefits-section.list.2": "Удобный мобильный доступ",
"dry-wash.landing.benefits-section.list.3": "Надежный и заслуживающий доверия",
"dry-wash.landing.footer.copyright": "\u00A9 {{currentYear}} DryMaster. Все права защищены",
"dry-wash.landing.footer.links.faq": "FAQ",
"dry-wash.landing.footer.links.privacy-policy": "Политика конфиденциальности",
"dry-wash.landing.footer.links.service-terms": "Условия обслуживания",
"dry-wash.landing.hero-section.description": "Ощутите максимальное удобство сухой мойки автомобилей, созданной для того, чтобы планета стала чище",
"dry-wash.landing.hero-section.headline": "Оживите свою поездку с помощью экологически чистого ухода!",
"dry-wash.landing.make-order-button": "Сделать заказ",
"dry-wash.landing.site-logo": "Логотип компании \u00ABDry Master\u00BB",
"dry-wash.landing.social-proof-section.heading": "Нас выбирают",
"dry-wash.notFound.title": "Страница не найдена", "dry-wash.notFound.title": "Страница не найдена",
"dry-wash.notFound.description": "К сожалению, запрашиваемая вами страница не существует.", "dry-wash.notFound.description": "К сожалению, запрашиваемая вами страница не существует.",
"dry-wash.notFound.button.back": " Вернуться на главную", "dry-wash.notFound.button.back": " Вернуться на главную",

549
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@brojs/cli": "^1.6.1", "@brojs/cli": "^1.6.3",
"@chakra-ui/icons": "^2.2.4", "@chakra-ui/icons": "^2.2.4",
"@chakra-ui/react": "^2.4.2", "@chakra-ui/react": "^2.4.2",
"@emotion/react": "^11.4.1", "@emotion/react": "^11.4.1",
@ -30,6 +30,7 @@
"i18next": "^23.16.4", "i18next": "^23.16.4",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-i18next": "^15.1.1",
"react-icons": "^5.3.0", "react-icons": "^5.3.0",
"react-router-dom": "^6.27.0" "react-router-dom": "^6.27.0"
}, },

24
src/__data__/urls.ts Normal file
View File

@ -0,0 +1,24 @@
import { generatePath } from "react-router-dom";
import { getNavigationValue } from "@brojs/cli";
import { Order } from "../models";
export const URLs = {
landing: {
url: getNavigationValue("dry-wash.main"),
getUrl() {
return this.url;
}
},
orderForm: {
url: getNavigationValue("dry-wash.create"),
getUrl() {
return this.url;
}
},
orderView: {
url: getNavigationValue("dry-wash.view.order"),
getUrl(orderId: Order.Id) {
return generatePath(this.url, { orderId });
}
}
};

View File

@ -1,4 +1,5 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
MdEco, MdEco,
MdMiscellaneousServices, MdMiscellaneousServices,
@ -10,33 +11,39 @@ import { CtaButton, PageSection } from '../';
import { ListItem } from './ListItem'; import { ListItem } from './ListItem';
export const BenefitsSection: FC = () => { 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 ( return (
<PageSection> <PageSection>
<VStack w='full' spacing={2}> <VStack w='full' spacing={2}>
<Heading as='h2'>Преимущества экологичной автомойки</Heading> <Heading as='h2'>{t('heading')}</Heading>
<Text> <Text>
Откройте для себя преимущества наших услуг по химчистке автомобилей {t('description')}
</Text> </Text>
</VStack> </VStack>
<List display='flex' flexDirection='column' spacing={3}> <List display='flex' flexDirection='column' spacing={3}>
{[ {listData.map((props, i) => (
{
Icon: MdEco,
children: 'Экологически безопасные продукты',
},
{
Icon: MdMiscellaneousServices,
children: 'Быстрое и эффективное обслуживание',
},
{
Icon: MdPlace,
children: 'Удобный мобильный доступ',
},
{
Icon: MdHandshake,
children: 'Надежный и заслуживающий доверия',
},
].map((props, i) => (
<ListItem key={i} {...props} /> <ListItem key={i} {...props} />
))} ))}
</List> </List>

View File

@ -2,7 +2,7 @@ import React, { FC, PropsWithChildren } from 'react';
import { ListIcon, ListItem as ChakraListItem } from '@chakra-ui/react'; import { ListIcon, ListItem as ChakraListItem } from '@chakra-ui/react';
import { IconType } from 'react-icons'; import { IconType } from 'react-icons';
type ListItemProps = PropsWithChildren & { export type ListItemProps = PropsWithChildren & {
Icon: IconType; Icon: IconType;
}; };

View File

@ -1 +1 @@
export { ListItem } from './ListItem'; export { type ListItemProps, ListItem } from './ListItem';

View File

@ -1,11 +1,20 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Link as RouterLink } from 'react-router-dom'; import { Link as RouterLink } from 'react-router-dom';
import { ButtonProps, Button } from '@chakra-ui/react'; import { ButtonProps, Button } from '@chakra-ui/react';
import { URLs } from '../../../__data__/urls';
export const CtaButton: FC<ButtonProps> = (props) => { export const CtaButton: FC<ButtonProps> = (props) => {
const { t } = useTranslation();
return ( return (
<Button as={RouterLink} to='/dry-wash/order-form' colorScheme='primary' {...props}> <Button
Сделать заказ as={RouterLink}
to={URLs.orderForm.getUrl()}
colorScheme='primary'
{...props}
>
{t('dry-wash.landing.make-order-button')}
</Button> </Button>
); );
}; };

View File

@ -1,8 +1,11 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Text } from '@chakra-ui/react'; import { Text } from '@chakra-ui/react';
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
export const Copyright: FC = () => { export const Copyright: FC = () => {
return <Text color='whiteAlpha.500'>&copy; {currentYear} DryMaster. Все права защищены</Text>; const { t } = useTranslation();
return <Text color='whiteAlpha.500'>{t('dry-wash.landing.footer.copyright', { currentYear })}</Text>;
}; };

View File

@ -1,20 +1,27 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, List, ListItem } from '@chakra-ui/react'; import { Link, List, ListItem } from '@chakra-ui/react';
import { Link as RouterLink } from 'react-router-dom'; import { Link as RouterLink } from 'react-router-dom';
import { SiteLogo, PageSection } from '../'; import { SiteLogo, PageSection } from '../';
import { Copyright } from './Copyright'; import { Copyright } from './Copyright';
export const Footer: FC = () => { export const Footer: FC = () => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.landing.footer.links',
});
const listData = [
{ to: '#', label: t('privacy-policy') },
{ to: '#', label: t('service-terms') },
{ to: '#', label: t('faq') },
];
return ( return (
<PageSection as='footer' py={5} bg='gray.700' color='white'> <PageSection as='footer' py={5} bg='gray.700' color='white'>
<SiteLogo /> <SiteLogo />
<Copyright /> <Copyright />
<List spacing={2}> <List spacing={2}>
{[ {listData.map(({ to, label }, i) => (
{ to: '#', label: 'Политика конфиденциальности' },
{ to: '#', label: 'Условия обслуживания' },
{ to: '#', label: 'FAQ' },
].map(({ to, label }, i) => (
<ListItem key={i}> <ListItem key={i}>
<Link as={RouterLink} to={to}> <Link as={RouterLink} to={to}>
{label} {label}

View File

@ -1,4 +1,5 @@
import React, { FC } from 'react'; 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, 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 '../';
@ -6,6 +7,10 @@ import { CtaButton, SiteLogo, PageSection } from '../';
type HeroSectionProps = Pick<BoxProps, 'flexShrink'>; type HeroSectionProps = Pick<BoxProps, 'flexShrink'>;
export const HeroSection: FC<HeroSectionProps> = ({ flexShrink }) => { export const HeroSection: FC<HeroSectionProps> = ({ flexShrink }) => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.landing.hero-section',
});
return ( return (
<Box flexShrink={flexShrink} as='header' pos='relative' zIndex={0}> <Box flexShrink={flexShrink} as='header' pos='relative' zIndex={0}>
<Box <Box
@ -41,19 +46,18 @@ export const HeroSection: FC<HeroSectionProps> = ({ flexShrink }) => {
color='white' color='white'
__css={{ textWrap: 'balance' }} __css={{ textWrap: 'balance' }}
> >
Оживите свою поездку с помощью экологически чистого ухода! {t('headline')}
</Heading> </Heading>
<Text <Text
textAlign='center' textAlign='center'
__css={{ textWrap: 'balance' }} __css={{ textWrap: 'balance' }}
color='white' color='white'
> >
Ощутите максимальное удобство сухой мойки автомобилей, созданной для {t('description')}
того, чтобы планета стала чище
</Text> </Text>
</VStack> </VStack>
<CtaButton size='lg' /> <CtaButton size='lg' />
</PageSection> </PageSection>
</Box> </Box>
); );
}; };

View File

@ -1,10 +1,12 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Image } from '@chakra-ui/react'; import { Image } from '@chakra-ui/react';
import { LogoSvg } from '../../../assets/icons'; import { LogoSvg } from '../../../assets/icons';
export const SiteLogo: FC = () => { export const SiteLogo: FC = () => {
return <Image src={LogoSvg} alt='Логотип компании "Сухой мастер"' w={40} />; const { t } = useTranslation();
return <Image src={LogoSvg} alt={t('dry-wash.landing.site-logo')} w={40} />;
}; };
// todo: add i18n for alt // todo: replace Image by SVG React component
// todo: replace Image by SVG React component

View File

@ -1,4 +1,5 @@
import React, { FC, useEffect, useState } from 'react'; import React, { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
Tab, Tab,
TabList, TabList,
@ -7,13 +8,17 @@ import {
Tabs, Tabs,
Text, Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { mockReviews } from '../../../../mocks/landing'; import { mockReview } from '../../../../mocks/landing';
import { Review } from '../../../../models';
import { ReviewCard } from './ReviewCard'; import { ReviewCard } from './ReviewCard';
const reviewsCount = mockReviews.length;
const SLIDE_CHANGE_INTERVAL = 5000; const SLIDE_CHANGE_INTERVAL = 5000;
export const ReviewsSlider: FC = () => { export const ReviewsSlider: FC = () => {
const { i18n } = useTranslation();
const mockReviews: Review.View[] = mockReview.getReviewsByLocale(i18n.language);
const reviewsCount = mockReviews.length;
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const [isSlideShowStopped, setIsSlideShowStopped] = useState(false); const [isSlideShowStopped, setIsSlideShowStopped] = useState(false);

View File

@ -1,15 +1,17 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { import { useTranslation } from 'react-i18next';
Heading, import { Heading, HStack } from '@chakra-ui/react';
HStack,
} from '@chakra-ui/react';
import { CtaButton, PageSection } from '../'; import { CtaButton, PageSection } from '../';
import { ReviewsSlider } from './ReviewsSlider'; import { ReviewsSlider } from './ReviewsSlider';
export const SocialProofSection: FC = () => { export const SocialProofSection: FC = () => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.landing.social-proof-section',
});
return ( return (
<PageSection> <PageSection>
<Heading as='h2'>Нас выбирают</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,34 +1,2 @@
type ReviewItem = { export * as mockOrder from './order';
id: string; export * as mockReview from './review';
firstname: string;
lastname: string;
picture: string;
text: string;
};
export const mockReviews: ReviewItem[] = [
{
firstname: 'Анна',
lastname: 'Смирнова',
picture: 'https://img.freepik.com/free-photo/indoor-portrait-beautiful-freckled-woman-with-dark-curly-hair-wears-fashionable-striped-shirt-rejoices-day-off-isolated-white-wall-curly-satisfied-woman-stands-indoor-alone_273609-15765.jpg',
text: "Недавно воспользовалась услугами сухой мойки автомобилей и осталась крайне удовлетворена. Процесс был проведён профессионально: сотрудники использовали качественные средства, которые не повредили лакокрасочное покрытие. Особенно впечатлила возможность мыть машину без воды, что не только экономит ресурсы, но и бережет окружающую среду. Рекомендую всем, кто заботится о своём автомобиле и экологии!"
},
{
firstname: 'Дмитрий',
lastname: 'Петров',
picture: 'https://img.freepik.com/free-photo/calm-handsome-curly-haired-boy-posing-isolated-light-grey-standing-still-looks-peaceful-wearing-casual-manner-youth-style-concept_176532-8831.jpg',
text: "Как же я рад, что нашел эту сухую мойку! Моя машина сияет, как новенькая! 🌟 Сначала был скептически настроен, думал, как же без воды можно отмыть всё это? Но результат превзошёл все ожидания! Ветеринар мойки профессионально подошёл к делу, и она теперь выглядит потрясающе. Если вы хотите, чтобы ваш автомобиль всегда выглядел на 100%, обязательно попробуйте!"
},
{
firstname: 'Алексей',
lastname: 'Сидоров',
picture: 'https://img.freepik.com/free-photo/waist-up-portrait-handsome-serious-unshaven-male-keeps-hands-together-dressed-dark-blue-shirt-has-talk-with-interlocutor-stands-against-white-wall-self-confident-man-freelancer_273609-16320.jpg',
text: "Сухая мойка автомобилей - интересное решение, которое я опробовал недавно. В целом остался доволен качеством работы. Однако, не все загрязнения удалось удалить с первого раза, но сотрудник предложил дополнительные услуги, что меня устроило. Плюс, большое внимание уделили защите поверхности, что тоже немаловажно. Думаю, в следующий раз снова воспользуюсь этой услугой."
},
{
firstname: 'Екатерина',
lastname: 'Иванова',
picture: 'https://img.freepik.com/free-photo/portrait-young-blonde-woman-with-plait-polka-dot-blouse_273609-10490.jpg',
text: "К сожалению, мой опыт с сухой мойкой автомобилей оказался неудачным. Ожидала увидеть чистую машину после процедуры, но многие участки остались незаделанными. Кроме того, процедура заняла больше времени, чем мне обещали. Возможно, в этом конкретном центре что-то пошло не так, но я бы не стала повторно обращаться за этой услугой."
},
].map((data, i) => ({ id: `review${i}`, ...data }));

View File

@ -0,0 +1,7 @@
import { Order } from "../../models";
export const orders: Order.View[] = [
{ id: 'id1' },
{ id: 'id2' },
{ id: 'id3' },
];

View File

@ -0,0 +1,64 @@
import { LANGUAGES, Review } from "../../models";
const addId = (items) => items.map((data, i) => ({ id: `review${i}`, ...data }));
const enReviews: Review.View[] = addId([
{
firstname: 'Anna',
lastname: 'Smirnova',
picture: 'https://img.freepik.com/free-photo/indoor-portrait-beautiful-freckled-woman-with-dark-curly-hair-wears-fashionable-striped-shirt-rejoices-day-off-isolated-white-wall-curly-satisfied-woman-stands-indoor-alone_273609-15765.jpg',
text: "I recently used the services of a dry car wash and was extremely satisfied. The process was carried out professionally: the staff used high-quality products that did not damage the paintwork. I was especially impressed by the ability to wash the car without water, which not only saves resources, but also protects the environment. I recommend it to everyone who cares about their car and the environment!"
},
{
firstname: 'Dmitry',
lastname: 'Petrov',
picture: 'https://img.freepik.com/free-photo/calm-handsome-curly-haired-boy-posing-isolated-light-grey-standing-still-looks-peaceful-wearing-casual-manner-youth-style-concept_176532-8831.jpg',
text: "I'm so glad I found this dry sink! My car is shining like new! 🌟 At first I was skeptical, I thought, how can you wash all this without water? But the result exceeded all expectations! Moika's veterinarian took a professional approach to the case, and she now looks amazing. If you want your car to always look 100%, be sure to try it!"
},
{
firstname: 'Alexey',
lastname: 'Sidorov',
picture: 'https://img.freepik.com/free-photo/waist-up-portrait-handsome-serious-unshaven-male-keeps-hands-together-dressed-dark-blue-shirt-has-talk-with-interlocutor-stands-against-white-wall-self-confident-man-freelancer_273609-16320.jpg',
text: "Dry car washing is an interesting solution that I tried out recently. Overall, I was satisfied with the quality of the work. However, not all the impurities were removed the first time, but the employee offered additional services, which suited me. Plus, a lot of attention was paid to surface protection, which is also important. I think I'll use this service again next time."
},
{
firstname: 'Ekaterina',
lastname: 'Ivanova',
picture: 'https://img.freepik.com/free-photo/portrait-young-blonde-woman-with-plait-polka-dot-blouse_273609-10490.jpg',
text: "Unfortunately, my experience with dry car washing turned out to be unsuccessful. I expected to see a clean car after the procedure, but many areas remained undone. Besides, the procedure took longer than I was promised. Perhaps something went wrong in this particular center, but I would not apply for this service again."
},
]);
const ruReviews: Review.View[] = addId([
{
firstname: 'Анна',
lastname: 'Смирнова',
picture: 'https://img.freepik.com/free-photo/indoor-portrait-beautiful-freckled-woman-with-dark-curly-hair-wears-fashionable-striped-shirt-rejoices-day-off-isolated-white-wall-curly-satisfied-woman-stands-indoor-alone_273609-15765.jpg',
text: "Недавно воспользовалась услугами сухой мойки автомобилей и осталась крайне удовлетворена. Процесс был проведён профессионально: сотрудники использовали качественные средства, которые не повредили лакокрасочное покрытие. Особенно впечатлила возможность мыть машину без воды, что не только экономит ресурсы, но и бережет окружающую среду. Рекомендую всем, кто заботится о своём автомобиле и экологии!"
},
{
firstname: 'Дмитрий',
lastname: 'Петров',
picture: 'https://img.freepik.com/free-photo/calm-handsome-curly-haired-boy-posing-isolated-light-grey-standing-still-looks-peaceful-wearing-casual-manner-youth-style-concept_176532-8831.jpg',
text: "Как же я рад, что нашел эту сухую мойку! Моя машина сияет, как новенькая! 🌟 Сначала был скептически настроен, думал, как же без воды можно отмыть всё это? Но результат превзошёл все ожидания! Ветеринар мойки профессионально подошёл к делу, и она теперь выглядит потрясающе. Если вы хотите, чтобы ваш автомобиль всегда выглядел на 100%, обязательно попробуйте!"
},
{
firstname: 'Алексей',
lastname: 'Сидоров',
picture: 'https://img.freepik.com/free-photo/waist-up-portrait-handsome-serious-unshaven-male-keeps-hands-together-dressed-dark-blue-shirt-has-talk-with-interlocutor-stands-against-white-wall-self-confident-man-freelancer_273609-16320.jpg',
text: "Сухая мойка автомобилей - интересное решение, которое я опробовал недавно. В целом остался доволен качеством работы. Однако, не все загрязнения удалось удалить с первого раза, но сотрудник предложил дополнительные услуги, что меня устроило. Плюс, большое внимание уделили защите поверхности, что тоже немаловажно. Думаю, в следующий раз снова воспользуюсь этой услугой."
},
{
firstname: 'Екатерина',
lastname: 'Иванова',
picture: 'https://img.freepik.com/free-photo/portrait-young-blonde-woman-with-plait-polka-dot-blouse_273609-10490.jpg',
text: "К сожалению, мой опыт с сухой мойкой автомобилей оказался неудачным. Ожидала увидеть чистую машину после процедуры, но многие участки остались незаделанными. Кроме того, процедура заняла больше времени, чем мне обещали. Возможно, в этом конкретном центре что-то пошло не так, но я бы не стала повторно обращаться за этой услугой."
},
]);
const reviewsMapByLocale: Record<LANGUAGES, Review.View[]> = {
[LANGUAGES.en]: enReviews,
[LANGUAGES.ru]: ruReviews
};
export const getReviewsByLocale = (locale: string) => reviewsMapByLocale[locale];

4
src/models/i18n.ts Normal file
View File

@ -0,0 +1,4 @@
export const enum LANGUAGES {
en = 'en',
ru = 'ru'
}

31
src/models/i18next.d.ts vendored Normal file
View File

@ -0,0 +1,31 @@
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;
type LanguageResource = CreateTree<keyof typeof defaultLocale>;
declare module "i18next" {
interface CustomTypeOptions {
resources: {
'~': LanguageResource
};
}
}

3
src/models/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './i18n';
export * as Order from './order';
export * as Review from './review';

5
src/models/order.ts Normal file
View File

@ -0,0 +1,5 @@
export type Id = string;
export type View = {
id: Id;
};

9
src/models/review.ts Normal file
View File

@ -0,0 +1,9 @@
export type Id = string;
export type View = {
id: Id;
firstname: string;
lastname: string;
picture: URL['href'];
text: string;
};

View File

@ -1,5 +1,5 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { Box, Container, VStack } from '@chakra-ui/react'; import { Container, VStack } from '@chakra-ui/react';
import { import {
BenefitsSection, BenefitsSection,
Footer, Footer,
@ -19,14 +19,12 @@ const Page: FC = () => {
bg='white' bg='white'
centerContent centerContent
> >
<VStack w='full' h='full' alignItems='stretch'> <VStack w='full' h='full' alignItems='stretch' flexGrow={1}>
<HeroSection flexShrink={0} /> <HeroSection flexShrink={0} />
<Box flexGrow={1}> <VStack as='main' flexGrow={1}>
<VStack as='main'> <BenefitsSection />
<BenefitsSection /> <SocialProofSection />
<SocialProofSection /> </VStack>
</VStack>
</Box>
<Footer /> <Footer />
</VStack> </VStack>
</Container> </Container>

View File

@ -1,7 +1,20 @@
import React from "react"; import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Button } from '@chakra-ui/react';
import { URLs } from '../../__data__/urls';
import { mockOrder } from '../../mocks/landing';
const Page = () => { const Page = () => {
return <h1>Order form </h1>; return (
<>
<h1>Order form</h1>
{mockOrder.orders.map(({ id }) => (
<Button key={id} as={RouterLink} to={URLs.orderView.getUrl(id)}>
Посмотреть заказ {id}
</Button>
))}
</>
);
}; };
export default Page; export default Page;

View File

@ -2,6 +2,7 @@ import React, { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import { PageSpinner } from './components'; import { PageSpinner } from './components';
import Arm from './pages/arm'; import Arm from './pages/arm';
import { URLs } from './__data__/urls';
import NotFound from './pages/notFound/notFound'; import NotFound from './pages/notFound/notFound';
const Landing = lazy(() => import('./pages/landing')); const Landing = lazy(() => import('./pages/landing'));
@ -12,11 +13,9 @@ const Routers = () => {
return ( return (
<Suspense fallback={<PageSpinner />}> <Suspense fallback={<PageSpinner />}>
<Routes> <Routes>
<Route path='/dry-wash'> <Route path={URLs.landing.url} element={<Landing />} />
<Route index element={<Landing />} /> <Route path={URLs.orderForm.url} element={<OrderForm />} />
<Route path='order-form' element={<OrderForm />} /> <Route path={URLs.orderView.url} element={<OrderView />} />
<Route path='order-view' element={<OrderView />} />
</Route>
<Route path='/dry-wash/arm/*' element={<Arm />}></Route> <Route path='/dry-wash/arm/*' element={<Arm />}></Route>
<Route path='*' element={<NotFound />} /> <Route path='*' element={<NotFound />} />
</Routes> </Routes>