diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..b416381 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,57 @@ +pipeline { + agent { + docker { + image 'node:20' + } + } + + stages { + stage('install (ci)') { + steps { + sh 'node -v' + sh 'npm -v' + script { + String tag = sh(returnStdout: true, script: 'git tag --contains').trim() + String branchName = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref HEAD').trim() + String commit = sh(returnStdout: true, script: 'git log -1 --oneline').trim() + String commitMsg = commit.substring(commit.indexOf(' ')).trim() + + if (tag) { + currentBuild.displayName = "#${BUILD_NUMBER}, tag ${tag}" + } else { + currentBuild.displayName = "#${BUILD_NUMBER}, branch ${branchName}" + } + + String author = sh(returnStdout: true, script: "git log -1 --pretty=format:'%an'").trim() + currentBuild.description = "${author}
${commitMsg}" + echo 'starting installing' + sh 'npm ci' + } + } + } + + stage('checks') { + parallel { + stage('eslint') { + steps { + sh 'npm run eslint' + } + } + + stage('build') { + steps { + sh 'npm run build' + } + } + } + } + + stage('clean-all') { + steps { + sh 'rm -rf .[!.]*' + sh 'rm -rf ./*' + sh 'ls -a' + } + } + } +} diff --git a/package-lock.json b/package-lock.json index d828fec..8015655 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,12 +15,14 @@ "@chakra-ui/react": "^2.4.2", "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", + "@fontsource/open-sans": "^5.1.0", "@types/react": "^18.3.12", "express": "^4.21.1", "framer-motion": "^6.2.8", "i18next": "^23.16.4", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.3.0", "react-router-dom": "^6.27.0" }, "devDependencies": { @@ -3313,6 +3315,12 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fontsource/open-sans": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.1.0.tgz", + "integrity": "sha512-g+mjF8gWUDwck9DrRCkhmFeEj7fskjtKZJKAQguVzSg93lc6ThakTHMRgs0dZfe5qBbktrV839tDrb4bIDyZSA==", + "license": "OFL-1.1" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -9091,6 +9099,15 @@ } } }, + "node_modules/react-icons": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", + "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -9971,6 +9988,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "license": "MIT", "engines": { "node": ">= 18.12.0" }, diff --git a/package.json b/package.json index 0ce05c1..52ada7a 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,14 @@ "@chakra-ui/react": "^2.4.2", "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", + "@fontsource/open-sans": "^5.1.0", "@types/react": "^18.3.12", "express": "^4.21.1", "framer-motion": "^6.2.8", "i18next": "^23.16.4", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.3.0", "react-router-dom": "^6.27.0" }, "devDependencies": { diff --git a/remote-assets/demo.mp4 b/remote-assets/demo.mp4 new file mode 100644 index 0000000..4af423d Binary files /dev/null and b/remote-assets/demo.mp4 differ diff --git a/src/assets/icons/dry-master-logo.svg b/src/assets/icons/dry-master-logo.svg new file mode 100644 index 0000000..a98dd4f --- /dev/null +++ b/src/assets/icons/dry-master-logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts new file mode 100644 index 0000000..405ca17 --- /dev/null +++ b/src/assets/icons/index.ts @@ -0,0 +1 @@ +export { default as LogoSvg } from './dry-master-logo.svg'; \ No newline at end of file diff --git a/src/assets/images/demo-video-poster.webp b/src/assets/images/demo-video-poster.webp new file mode 100644 index 0000000..0d2016d Binary files /dev/null and b/src/assets/images/demo-video-poster.webp differ diff --git a/src/assets/images/index.ts b/src/assets/images/index.ts new file mode 100644 index 0000000..9e009f9 --- /dev/null +++ b/src/assets/images/index.ts @@ -0,0 +1 @@ +export { default as DemoVideoPosterImg } from './demo-video-poster.webp'; \ No newline at end of file diff --git a/src/components/PageSpinner/PageSpinner.tsx b/src/components/PageSpinner/PageSpinner.tsx new file mode 100644 index 0000000..0f0daff --- /dev/null +++ b/src/components/PageSpinner/PageSpinner.tsx @@ -0,0 +1,16 @@ +import React, { FC } from 'react'; +import { Flex, Spinner } from '@chakra-ui/react'; + +export const PageSpinner: FC = () => { + return ( + + + + ); +}; diff --git a/src/components/PageSpinner/index.ts b/src/components/PageSpinner/index.ts new file mode 100644 index 0000000..31d2b5c --- /dev/null +++ b/src/components/PageSpinner/index.ts @@ -0,0 +1 @@ +export { PageSpinner } from './PageSpinner'; \ No newline at end of file diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..3a75cc1 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1 @@ +export * from './PageSpinner'; \ No newline at end of file diff --git a/src/components/landing/BenefitsSection/BenefitsSection.tsx b/src/components/landing/BenefitsSection/BenefitsSection.tsx new file mode 100644 index 0000000..6132df2 --- /dev/null +++ b/src/components/landing/BenefitsSection/BenefitsSection.tsx @@ -0,0 +1,48 @@ +import React, { FC } from 'react'; +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'; + +export const BenefitsSection: FC = () => { + return ( + + + Преимущества экологичной автомойки + + Откройте для себя преимущества наших услуг по химчистке автомобилей + + + + {[ + { + Icon: MdEco, + children: 'Экологически безопасные продукты', + }, + { + Icon: MdMiscellaneousServices, + children: 'Быстрое и эффективное обслуживание', + }, + { + Icon: MdPlace, + children: 'Удобный мобильный доступ', + }, + { + Icon: MdHandshake, + children: 'Надежный и заслуживающий доверия', + }, + ].map((props, i) => ( + + ))} + + + + + + ); +}; diff --git a/src/components/landing/BenefitsSection/ListItem/ListItem.tsx b/src/components/landing/BenefitsSection/ListItem/ListItem.tsx new file mode 100644 index 0000000..4c9343d --- /dev/null +++ b/src/components/landing/BenefitsSection/ListItem/ListItem.tsx @@ -0,0 +1,16 @@ +import React, { FC, PropsWithChildren } from 'react'; +import { ListIcon, ListItem as ChakraListItem } from '@chakra-ui/react'; +import { IconType } from 'react-icons'; + +type ListItemProps = PropsWithChildren & { + Icon: IconType; +}; + +export const ListItem: FC = ({ Icon, children }) => { + return ( + + + {children} + + ); +}; diff --git a/src/components/landing/BenefitsSection/ListItem/index.ts b/src/components/landing/BenefitsSection/ListItem/index.ts new file mode 100644 index 0000000..de932bf --- /dev/null +++ b/src/components/landing/BenefitsSection/ListItem/index.ts @@ -0,0 +1 @@ +export { ListItem } from './ListItem'; \ No newline at end of file diff --git a/src/components/landing/BenefitsSection/index.ts b/src/components/landing/BenefitsSection/index.ts new file mode 100644 index 0000000..d10431b --- /dev/null +++ b/src/components/landing/BenefitsSection/index.ts @@ -0,0 +1 @@ +export { BenefitsSection } from './BenefitsSection'; \ No newline at end of file diff --git a/src/components/landing/CtaButton/CtaButton.tsx b/src/components/landing/CtaButton/CtaButton.tsx new file mode 100644 index 0000000..a1168f3 --- /dev/null +++ b/src/components/landing/CtaButton/CtaButton.tsx @@ -0,0 +1,11 @@ +import React, { FC } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { ButtonProps, Button } from '@chakra-ui/react'; + +export const CtaButton: FC = (props) => { + return ( + + ); +}; diff --git a/src/components/landing/CtaButton/index.ts b/src/components/landing/CtaButton/index.ts new file mode 100644 index 0000000..08818cf --- /dev/null +++ b/src/components/landing/CtaButton/index.ts @@ -0,0 +1 @@ +export { CtaButton } from './CtaButton'; \ No newline at end of file diff --git a/src/components/landing/Footer/Copyright/Copyright.tsx b/src/components/landing/Footer/Copyright/Copyright.tsx new file mode 100644 index 0000000..bc26f23 --- /dev/null +++ b/src/components/landing/Footer/Copyright/Copyright.tsx @@ -0,0 +1,8 @@ +import React, { FC } from 'react'; +import { Text } from '@chakra-ui/react'; + +const currentYear = new Date().getFullYear(); + +export const Copyright: FC = () => { + return © {currentYear} DryMaster. Все права защищены; +}; diff --git a/src/components/landing/Footer/Copyright/index.ts b/src/components/landing/Footer/Copyright/index.ts new file mode 100644 index 0000000..f80e480 --- /dev/null +++ b/src/components/landing/Footer/Copyright/index.ts @@ -0,0 +1 @@ +export { Copyright } from './Copyright'; \ No newline at end of file diff --git a/src/components/landing/Footer/Footer.tsx b/src/components/landing/Footer/Footer.tsx new file mode 100644 index 0000000..26b8462 --- /dev/null +++ b/src/components/landing/Footer/Footer.tsx @@ -0,0 +1,27 @@ +import React, { FC } from 'react'; +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 = () => { + return ( + + + + + {[ + { to: '#', label: 'Политика конфиденциальности' }, + { to: '#', label: 'Условия обслуживания' }, + { to: '#', label: 'FAQ' }, + ].map(({ to, label }, i) => ( + + + {label} + + + ))} + + + ); +}; diff --git a/src/components/landing/Footer/index.ts b/src/components/landing/Footer/index.ts new file mode 100644 index 0000000..f6e9523 --- /dev/null +++ b/src/components/landing/Footer/index.ts @@ -0,0 +1 @@ +export { Footer } from './Footer'; \ No newline at end of file diff --git a/src/components/landing/HeroSection/HeroSection.tsx b/src/components/landing/HeroSection/HeroSection.tsx new file mode 100644 index 0000000..412eab0 --- /dev/null +++ b/src/components/landing/HeroSection/HeroSection.tsx @@ -0,0 +1,59 @@ +import React, { FC } from 'react'; +import { Box, Heading, Text, Center, VStack, BoxProps } from '@chakra-ui/react'; +import { DemoVideoPosterImg } from '../../../assets/images'; +import { CtaButton, SiteLogo, PageSection } from '../'; + +type HeroSectionProps = Pick; + +export const HeroSection: FC = ({ flexShrink }) => { + return ( + + + +
+ +
+ + + Оживите свою поездку с помощью экологически чистого ухода! + + + Ощутите максимальное удобство сухой мойки автомобилей, созданной для + того, чтобы планета стала чище + + + +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/landing/HeroSection/index.ts b/src/components/landing/HeroSection/index.ts new file mode 100644 index 0000000..ac31a65 --- /dev/null +++ b/src/components/landing/HeroSection/index.ts @@ -0,0 +1 @@ +export { HeroSection } from './HeroSection'; \ No newline at end of file diff --git a/src/components/landing/PageSection/PageSection.tsx b/src/components/landing/PageSection/PageSection.tsx new file mode 100644 index 0000000..23c04fe --- /dev/null +++ b/src/components/landing/PageSection/PageSection.tsx @@ -0,0 +1,12 @@ +import React, { FC, PropsWithChildren } from 'react'; +import { StackProps, VStack } from '@chakra-ui/react'; + +type PageSectionProps = StackProps & PropsWithChildren; + +export const PageSection: FC = ({ children, ...restProps }) => { + return ( + + {children} + + ); +}; diff --git a/src/components/landing/PageSection/index.ts b/src/components/landing/PageSection/index.ts new file mode 100644 index 0000000..5289ae0 --- /dev/null +++ b/src/components/landing/PageSection/index.ts @@ -0,0 +1 @@ +export { PageSection } from './PageSection'; \ No newline at end of file diff --git a/src/components/landing/SiteLogo/SiteLogo.tsx b/src/components/landing/SiteLogo/SiteLogo.tsx new file mode 100644 index 0000000..449a117 --- /dev/null +++ b/src/components/landing/SiteLogo/SiteLogo.tsx @@ -0,0 +1,10 @@ +import React, { FC } from 'react'; +import { Image } from '@chakra-ui/react'; +import { LogoSvg } from '../../../assets/icons'; + +export const SiteLogo: FC = () => { + return Логотип компании "Сухой мастер"; +}; + +// todo: add i18n for alt +// todo: replace Image by SVG React component \ No newline at end of file diff --git a/src/components/landing/SiteLogo/index.ts b/src/components/landing/SiteLogo/index.ts new file mode 100644 index 0000000..48bcc7d --- /dev/null +++ b/src/components/landing/SiteLogo/index.ts @@ -0,0 +1 @@ +export { SiteLogo } from './SiteLogo'; diff --git a/src/components/landing/SocialProofSection/ReviewsSlider/ReviewCard/ReviewCard.tsx b/src/components/landing/SocialProofSection/ReviewsSlider/ReviewCard/ReviewCard.tsx new file mode 100644 index 0000000..fcf3697 --- /dev/null +++ b/src/components/landing/SocialProofSection/ReviewsSlider/ReviewCard/ReviewCard.tsx @@ -0,0 +1,37 @@ +import React, { FC } from 'react'; +import { Card, Avatar, Text } from '@chakra-ui/react'; + +type ReviewCardProps = { + firstname: string; + lastname: string; + picture: string; + text: string; +}; + +export const ReviewCard: FC = ({ + firstname, + lastname, + picture, + text, +}) => { + const name = [firstname, lastname].join(' '); + + return ( + + + + {text} + + + ); +}; diff --git a/src/components/landing/SocialProofSection/ReviewsSlider/ReviewCard/index.ts b/src/components/landing/SocialProofSection/ReviewsSlider/ReviewCard/index.ts new file mode 100644 index 0000000..1582f4d --- /dev/null +++ b/src/components/landing/SocialProofSection/ReviewsSlider/ReviewCard/index.ts @@ -0,0 +1 @@ +export { ReviewCard } from './ReviewCard'; \ No newline at end of file diff --git a/src/components/landing/SocialProofSection/ReviewsSlider/ReviewsSlider.tsx b/src/components/landing/SocialProofSection/ReviewsSlider/ReviewsSlider.tsx new file mode 100644 index 0000000..be89118 --- /dev/null +++ b/src/components/landing/SocialProofSection/ReviewsSlider/ReviewsSlider.tsx @@ -0,0 +1,73 @@ +import React, { FC, useEffect, useState } from 'react'; +import { + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + Text, +} from '@chakra-ui/react'; +import { mockReviews } from '../../../../mocks/landing'; +import { ReviewCard } from './ReviewCard'; + +const reviewsCount = mockReviews.length; +const SLIDE_CHANGE_INTERVAL = 5000; + +export const ReviewsSlider: FC = () => { + const [activeTab, setActiveTab] = useState(0); + const [isSlideShowStopped, setIsSlideShowStopped] = useState(false); + + useEffect(() => { + const timer = setInterval(() => { + const newActiveTab = (activeTab + 1) % reviewsCount; + setActiveTab(newActiveTab); + }, SLIDE_CHANGE_INTERVAL); + + if (isSlideShowStopped) { + clearInterval(timer); + } + + return () => { + clearInterval(timer); + }; + }, [activeTab]); + + return ( + { + setIsSlideShowStopped(true); + setActiveTab(selectedTab); + }} + display='flex' + flexDir='column' + alignItems='center' + variant='soft-rounded' + colorScheme='gray' + > + + {mockReviews.map(({ id }, i) => ( + + {i} + + ))} + + + {mockReviews.map(({ id, ...reviewProps }) => ( + + + + ))} + + + ); +}; diff --git a/src/components/landing/SocialProofSection/ReviewsSlider/index.ts b/src/components/landing/SocialProofSection/ReviewsSlider/index.ts new file mode 100644 index 0000000..773341e --- /dev/null +++ b/src/components/landing/SocialProofSection/ReviewsSlider/index.ts @@ -0,0 +1 @@ +export { ReviewsSlider } from './ReviewsSlider'; \ No newline at end of file diff --git a/src/components/landing/SocialProofSection/SocialProofSection.tsx b/src/components/landing/SocialProofSection/SocialProofSection.tsx new file mode 100644 index 0000000..d2ec90a --- /dev/null +++ b/src/components/landing/SocialProofSection/SocialProofSection.tsx @@ -0,0 +1,19 @@ +import React, { FC } from 'react'; +import { + Heading, + HStack, +} from '@chakra-ui/react'; +import { CtaButton, PageSection } from '../'; +import { ReviewsSlider } from './ReviewsSlider'; + +export const SocialProofSection: FC = () => { + return ( + + Нас выбирают + + + + + + ); +}; diff --git a/src/components/landing/SocialProofSection/index.ts b/src/components/landing/SocialProofSection/index.ts new file mode 100644 index 0000000..e20037f --- /dev/null +++ b/src/components/landing/SocialProofSection/index.ts @@ -0,0 +1 @@ +export { SocialProofSection } from './SocialProofSection'; \ No newline at end of file diff --git a/src/components/landing/index.ts b/src/components/landing/index.ts new file mode 100644 index 0000000..54cda58 --- /dev/null +++ b/src/components/landing/index.ts @@ -0,0 +1,7 @@ +export * from './CtaButton'; +export * from './PageSection'; +export * from './BenefitsSection'; // CtaButton, PageSection +export * from './SocialProofSection'; // CtaButton, PageSection +export * from './SiteLogo'; +export * from './Footer'; // PageSection, SiteLogo +export * from './HeroSection'; // CtaButton, PageSection, SiteLogo \ No newline at end of file diff --git a/src/containers/LandingThemeProvider/Fonts.tsx b/src/containers/LandingThemeProvider/Fonts.tsx new file mode 100644 index 0000000..5e72244 --- /dev/null +++ b/src/containers/LandingThemeProvider/Fonts.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Global } from '@emotion/react'; +import '@fontsource/open-sans/400.css'; +import '@fontsource/open-sans/700.css'; + +const Fonts = () => ( + +); + +export default Fonts; diff --git a/src/containers/LandingThemeProvider/LandingThemeProvider.tsx b/src/containers/LandingThemeProvider/LandingThemeProvider.tsx new file mode 100644 index 0000000..c52bf1d --- /dev/null +++ b/src/containers/LandingThemeProvider/LandingThemeProvider.tsx @@ -0,0 +1,13 @@ +import React, { FC, PropsWithChildren } from 'react'; +import { ChakraProvider } from '@chakra-ui/react'; +import { default as landingTheme } from './theme-config'; +import Fonts from './Fonts'; + +export const LandingThemeProvider: FC = ({ children }) => { + return ( + + + {children} + + ); +}; diff --git a/src/containers/LandingThemeProvider/index.ts b/src/containers/LandingThemeProvider/index.ts new file mode 100644 index 0000000..b68aca1 --- /dev/null +++ b/src/containers/LandingThemeProvider/index.ts @@ -0,0 +1 @@ +export { LandingThemeProvider } from './LandingThemeProvider'; \ No newline at end of file diff --git a/src/containers/LandingThemeProvider/theme-config.ts b/src/containers/LandingThemeProvider/theme-config.ts new file mode 100644 index 0000000..43a0676 --- /dev/null +++ b/src/containers/LandingThemeProvider/theme-config.ts @@ -0,0 +1,42 @@ +import { extendTheme } from '@chakra-ui/react'; + +const overrides = { + colors: { + primary: { + 50: "#F1F8ED", + 100: "#D9EACC", + 200: "#C0DDAC", + 300: "#A8D08B", + 400: "#8FC26B", + 500: "#77B54A", + 600: "#5F913B", + 700: "#476D2C", + 800: "#2F481E", + 900: "#18240F" + }, + secondary: { + 50: "#E7F7FD", + 100: "#BCEAFB", + 200: "#91DCF8", + 300: "#66CEF5", + 400: "#3BC1F2", + 500: "#0FB3F0", + 600: "#0C8FC0", + 700: "#096B90", + 800: "#064860", + 900: "#032430" + } + }, + fonts: { + heading: `'Open Sans', sans-serif`, + }, + styles: { + global: { + body: { + bg: 'gray.100' + } + }, + }, +}; + +export default extendTheme(overrides); \ No newline at end of file diff --git a/src/containers/index.ts b/src/containers/index.ts new file mode 100644 index 0000000..f2db842 --- /dev/null +++ b/src/containers/index.ts @@ -0,0 +1 @@ +export * from './LandingThemeProvider'; \ No newline at end of file diff --git a/src/mocks/landing/index.ts b/src/mocks/landing/index.ts new file mode 100644 index 0000000..e6862bc --- /dev/null +++ b/src/mocks/landing/index.ts @@ -0,0 +1,34 @@ +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 })); \ No newline at end of file diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx index 96ee58e..1097a50 100644 --- a/src/pages/landing/index.tsx +++ b/src/pages/landing/index.tsx @@ -1,7 +1,37 @@ -import React from "react"; +import React, { FC } from 'react'; +import { Box, Container, VStack } from '@chakra-ui/react'; +import { + BenefitsSection, + Footer, + HeroSection, + SocialProofSection, +} from '../../components/landing'; +import { LandingThemeProvider } from '../../containers'; -const Page = () => { - return

Landing

; +const Page: FC = () => { + return ( + + + + + + + + + + +