feat/landing #30
							
								
								
									
										17
									
								
								locales/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								locales/en.json
									
									
									
									
									
										Normal 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" | ||||
| } | ||||
| @ -29,6 +29,21 @@ | ||||
|   "dry-wash.arm.master.sideBar.title": " Сухой мастер", | ||||
|   "dry-wash.arm.master.sideBar.title.master": "Мастера", | ||||
|   "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.description": "К сожалению, запрашиваемая вами страница не существует.", | ||||
|   "dry-wash.notFound.button.back": " Вернуться на главную", | ||||
|  | ||||
							
								
								
									
										549
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										549
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -17,7 +17,7 @@ | ||||
|     "author": "", | ||||
|     "license": "ISC", | ||||
|     "dependencies": { | ||||
|         "@brojs/cli": "^1.6.1", | ||||
|         "@brojs/cli": "^1.6.3", | ||||
|         "@chakra-ui/icons": "^2.2.4", | ||||
|         "@chakra-ui/react": "^2.4.2", | ||||
|         "@emotion/react": "^11.4.1", | ||||
| @ -30,6 +30,7 @@ | ||||
|         "i18next": "^23.16.4", | ||||
|         "react": "^18.3.1", | ||||
|         "react-dom": "^18.3.1", | ||||
|         "react-i18next": "^15.1.1", | ||||
|         "react-icons": "^5.3.0", | ||||
|         "react-router-dom": "^6.27.0" | ||||
|     }, | ||||
|  | ||||
							
								
								
									
										24
									
								
								src/__data__/urls.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/__data__/urls.ts
									
									
									
									
									
										Normal 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 }); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| @ -1,4 +1,5 @@ | ||||
| import React, { FC } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { | ||||
|   MdEco, | ||||
|   MdMiscellaneousServices, | ||||
| @ -10,33 +11,39 @@ import { CtaButton, PageSection } from '../'; | ||||
| import { ListItem } from './ListItem'; | ||||
| 
 | ||||
| export const BenefitsSection: FC = () => { | ||||
|   return ( | ||||
|     <PageSection> | ||||
|       <VStack w='full' spacing={2}> | ||||
|         <Heading as='h2'>Преимущества экологичной автомойки</Heading> | ||||
|         <Text> | ||||
|           Откройте для себя преимущества наших услуг по химчистке автомобилей | ||||
|         </Text> | ||||
|       </VStack> | ||||
|       <List display='flex' flexDirection='column' spacing={3}> | ||||
|         {[ | ||||
|   const { t } = useTranslation('~', { | ||||
|     keyPrefix: 'dry-wash.landing.benefits-section', | ||||
|   }); | ||||
| 
 | ||||
|   const listData = [ | ||||
|     { | ||||
|       Icon: MdEco, | ||||
|             children: 'Экологически безопасные продукты', | ||||
|       children: t('list.0'), | ||||
|     }, | ||||
|     { | ||||
|       Icon: MdMiscellaneousServices, | ||||
|             children: 'Быстрое и эффективное обслуживание', | ||||
|       children: t('list.1'), | ||||
|     }, | ||||
|     { | ||||
|       Icon: MdPlace, | ||||
|             children: 'Удобный мобильный доступ', | ||||
|       children: t('list.2'), | ||||
|     }, | ||||
|     { | ||||
|       Icon: MdHandshake, | ||||
|             children: 'Надежный и заслуживающий доверия', | ||||
|       children: t('list.3'), | ||||
|     }, | ||||
|         ].map((props, i) => ( | ||||
|   ]; | ||||
| 
 | ||||
|   return ( | ||||
|     <PageSection> | ||||
|       <VStack w='full' spacing={2}> | ||||
|         <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> | ||||
|  | ||||
| @ -2,7 +2,7 @@ import React, { FC, PropsWithChildren } from 'react'; | ||||
| import { ListIcon, ListItem as ChakraListItem } from '@chakra-ui/react'; | ||||
| import { IconType } from 'react-icons'; | ||||
| 
 | ||||
| type ListItemProps = PropsWithChildren & { | ||||
| export type ListItemProps = PropsWithChildren & { | ||||
|   Icon: IconType; | ||||
| }; | ||||
| 
 | ||||
|  | ||||
| @ -1 +1 @@ | ||||
| export { ListItem } from './ListItem'; | ||||
| export { type ListItemProps, ListItem } from './ListItem'; | ||||
| @ -1,11 +1,20 @@ | ||||
| import React, { FC } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { Link as RouterLink } from 'react-router-dom'; | ||||
| import { ButtonProps, Button } from '@chakra-ui/react'; | ||||
| import { URLs } from '../../../__data__/urls'; | ||||
| 
 | ||||
| export const CtaButton: FC<ButtonProps> = (props) => { | ||||
|   const { t } = useTranslation(); | ||||
| 
 | ||||
|   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> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| @ -1,8 +1,11 @@ | ||||
| import React, { FC } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { Text } from '@chakra-ui/react'; | ||||
| 
 | ||||
| const currentYear = new Date().getFullYear(); | ||||
| 
 | ||||
| export const Copyright: FC = () => { | ||||
|   return <Text color='whiteAlpha.500'>© {currentYear} DryMaster. Все права защищены</Text>; | ||||
|   const { t } = useTranslation(); | ||||
| 
 | ||||
|   return <Text color='whiteAlpha.500'>{t('dry-wash.landing.footer.copyright', { currentYear })}</Text>; | ||||
| }; | ||||
|  | ||||
| @ -1,20 +1,27 @@ | ||||
| import React, { FC } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { Link, List, ListItem } from '@chakra-ui/react'; | ||||
| import { Link as RouterLink } from 'react-router-dom'; | ||||
| import { SiteLogo, PageSection } from '../'; | ||||
| import { Copyright } from './Copyright'; | ||||
| 
 | ||||
| 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 ( | ||||
|     <PageSection as='footer' py={5} bg='gray.700' color='white'> | ||||
|       <SiteLogo /> | ||||
|       <Copyright /> | ||||
|       <List spacing={2}> | ||||
|         {[ | ||||
|           { to: '#', label: 'Политика конфиденциальности' }, | ||||
|           { to: '#', label: 'Условия обслуживания' }, | ||||
|           { to: '#', label: 'FAQ' }, | ||||
|         ].map(({ to, label }, i) => ( | ||||
|         {listData.map(({ to, label }, i) => ( | ||||
|           <ListItem key={i}> | ||||
|             <Link as={RouterLink} to={to}> | ||||
|               {label} | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import React, { FC } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { Box, Heading, Text, Center, VStack, BoxProps } from '@chakra-ui/react'; | ||||
| import { DemoVideoPosterImg } from '../../../assets/images'; | ||||
| import { CtaButton, SiteLogo, PageSection } from '../'; | ||||
| @ -6,6 +7,10 @@ import { CtaButton, SiteLogo, PageSection } from '../'; | ||||
| type HeroSectionProps = Pick<BoxProps, 'flexShrink'>; | ||||
| 
 | ||||
| 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 | ||||
| @ -41,15 +46,14 @@ export const HeroSection: FC<HeroSectionProps> = ({ flexShrink }) => { | ||||
|             color='white' | ||||
|             __css={{ textWrap: 'balance' }} | ||||
|           > | ||||
|             Оживите свою поездку с помощью экологически чистого ухода! | ||||
|             {t('headline')} | ||||
|           </Heading> | ||||
|           <Text | ||||
|             textAlign='center' | ||||
|             __css={{ textWrap: 'balance' }} | ||||
|             color='white' | ||||
|           > | ||||
|             Ощутите максимальное удобство сухой мойки автомобилей, созданной для | ||||
|             того, чтобы планета стала чище | ||||
|             {t('description')} | ||||
|           </Text> | ||||
|         </VStack> | ||||
|         <CtaButton size='lg' /> | ||||
|  | ||||
| @ -1,10 +1,12 @@ | ||||
| import React, { FC } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { Image } from '@chakra-ui/react'; | ||||
| import { LogoSvg } from '../../../assets/icons'; | ||||
| 
 | ||||
| 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
 | ||||
| @ -1,4 +1,5 @@ | ||||
| import React, { FC, useEffect, useState } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { | ||||
|   Tab, | ||||
|   TabList, | ||||
| @ -7,13 +8,17 @@ import { | ||||
|   Tabs, | ||||
|   Text, | ||||
| } from '@chakra-ui/react'; | ||||
| import { mockReviews } from '../../../../mocks/landing'; | ||||
| import { mockReview } from '../../../../mocks/landing'; | ||||
| import { Review } from '../../../../models'; | ||||
| import { ReviewCard } from './ReviewCard'; | ||||
| 
 | ||||
| const reviewsCount = mockReviews.length; | ||||
| const SLIDE_CHANGE_INTERVAL = 5000; | ||||
| 
 | ||||
| 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 [isSlideShowStopped, setIsSlideShowStopped] = useState(false); | ||||
| 
 | ||||
|  | ||||
| @ -1,15 +1,17 @@ | ||||
| import React, { FC } from 'react'; | ||||
| import { | ||||
|   Heading, | ||||
|   HStack, | ||||
| } from '@chakra-ui/react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { Heading, HStack } from '@chakra-ui/react'; | ||||
| import { CtaButton, PageSection } from '../'; | ||||
| import { ReviewsSlider } from './ReviewsSlider'; | ||||
| 
 | ||||
| export const SocialProofSection: FC = () => { | ||||
|   const { t } = useTranslation('~', { | ||||
|     keyPrefix: 'dry-wash.landing.social-proof-section', | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <PageSection> | ||||
|       <Heading as='h2'>Нас выбирают</Heading> | ||||
|       <Heading as='h2'>{t('heading')}</Heading> | ||||
|       <ReviewsSlider /> | ||||
|       <HStack w='full' justify='flex-end'> | ||||
|         <CtaButton /> | ||||
|  | ||||
| @ -1,34 +1,2 @@ | ||||
| type ReviewItem = { | ||||
|   id: string; | ||||
|   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 })); | ||||
| export * as mockOrder from './order'; | ||||
| export * as mockReview from './review'; | ||||
							
								
								
									
										7
									
								
								src/mocks/landing/order.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/mocks/landing/order.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| import { Order } from "../../models"; | ||||
| 
 | ||||
| export const orders: Order.View[] = [ | ||||
|   { id: 'id1' }, | ||||
|   { id: 'id2' }, | ||||
|   { id: 'id3' }, | ||||
| ]; | ||||
							
								
								
									
										64
									
								
								src/mocks/landing/review.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/mocks/landing/review.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										4
									
								
								src/models/i18n.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| export const enum LANGUAGES { | ||||
|   en = 'en', | ||||
|   ru = 'ru' | ||||
| } | ||||
							
								
								
									
										31
									
								
								src/models/i18next.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/models/i18next.d.ts
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										3
									
								
								src/models/index.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										5
									
								
								src/models/order.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| export type Id = string; | ||||
| 
 | ||||
| export type View = { | ||||
|   id: Id; | ||||
| }; | ||||
							
								
								
									
										9
									
								
								src/models/review.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/models/review.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| export type Id = string; | ||||
| 
 | ||||
| export type View = { | ||||
|   id: Id; | ||||
|   firstname: string; | ||||
|   lastname: string; | ||||
|   picture: URL['href']; | ||||
|   text: string; | ||||
| }; | ||||
| @ -1,5 +1,5 @@ | ||||
| import React, { FC } from 'react'; | ||||
| import { Box, Container, VStack } from '@chakra-ui/react'; | ||||
| import { Container, VStack } from '@chakra-ui/react'; | ||||
| import { | ||||
|   BenefitsSection, | ||||
|   Footer, | ||||
| @ -19,14 +19,12 @@ const Page: FC = () => { | ||||
|         bg='white' | ||||
|         centerContent | ||||
|       > | ||||
|         <VStack w='full' h='full' alignItems='stretch'> | ||||
|         <VStack w='full' h='full' alignItems='stretch' flexGrow={1}> | ||||
|           <HeroSection flexShrink={0} /> | ||||
|           <Box flexGrow={1}> | ||||
|             <VStack as='main'> | ||||
|           <VStack as='main' flexGrow={1}> | ||||
|             <BenefitsSection /> | ||||
|             <SocialProofSection /> | ||||
|           </VStack> | ||||
|           </Box> | ||||
|           <Footer /> | ||||
|         </VStack> | ||||
|       </Container> | ||||
|  | ||||
| @ -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 = () => { | ||||
|   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; | ||||
|  | ||||
| @ -2,6 +2,7 @@ import React, { lazy, Suspense } from 'react'; | ||||
| import { Routes, Route } from 'react-router-dom'; | ||||
| import { PageSpinner } from './components'; | ||||
| import Arm from './pages/arm'; | ||||
| import { URLs } from './__data__/urls'; | ||||
| import NotFound from './pages/notFound/notFound'; | ||||
| 
 | ||||
| const Landing = lazy(() => import('./pages/landing')); | ||||
| @ -12,11 +13,9 @@ const Routers = () => { | ||||
|   return ( | ||||
|     <Suspense fallback={<PageSpinner />}> | ||||
|       <Routes> | ||||
|         <Route path='/dry-wash'> | ||||
|           <Route index element={<Landing />} /> | ||||
|           <Route path='order-form' element={<OrderForm />} /> | ||||
|           <Route path='order-view' element={<OrderView />} /> | ||||
|         </Route> | ||||
|         <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> | ||||
|         <Route path='*' element={<NotFound />} /> | ||||
|       </Routes> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user