feat: landing (#7) #21
							
								
								
									
										57
									
								
								Jenkinsfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								Jenkinsfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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}<br />${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' | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -14,11 +14,13 @@ | |||||||
|                 "@chakra-ui/react": "^2.4.2", |                 "@chakra-ui/react": "^2.4.2", | ||||||
|                 "@emotion/react": "^11.4.1", |                 "@emotion/react": "^11.4.1", | ||||||
|                 "@emotion/styled": "^11.3.0", |                 "@emotion/styled": "^11.3.0", | ||||||
|  |                 "@fontsource/open-sans": "^5.1.0", | ||||||
|                 "@types/react": "^18.3.12", |                 "@types/react": "^18.3.12", | ||||||
|                 "express": "^4.21.1", |                 "express": "^4.21.1", | ||||||
|                 "framer-motion": "^6.2.8", |                 "framer-motion": "^6.2.8", | ||||||
|                 "react": "^18.3.1", |                 "react": "^18.3.1", | ||||||
|                 "react-dom": "^18.3.1", |                 "react-dom": "^18.3.1", | ||||||
|  |                 "react-icons": "^5.3.0", | ||||||
|                 "react-router-dom": "^6.27.0" |                 "react-router-dom": "^6.27.0" | ||||||
|             }, |             }, | ||||||
|             "devDependencies": { |             "devDependencies": { | ||||||
| @ -3303,6 +3305,12 @@ | |||||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" |                 "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": { |         "node_modules/@humanfs/core": { | ||||||
|             "version": "0.19.1", |             "version": "0.19.1", | ||||||
|             "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", |             "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", | ||||||
| @ -9073,6 +9081,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": { |         "node_modules/react-is": { | ||||||
|             "version": "16.13.1", |             "version": "16.13.1", | ||||||
|             "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", |             "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", | ||||||
| @ -9953,6 +9970,7 @@ | |||||||
|             "version": "4.0.0", |             "version": "4.0.0", | ||||||
|             "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", |             "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", | ||||||
|             "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", |             "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", | ||||||
|  |             "license": "MIT", | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">= 18.12.0" |                 "node": ">= 18.12.0" | ||||||
|             }, |             }, | ||||||
|  | |||||||
| @ -22,11 +22,13 @@ | |||||||
|         "@chakra-ui/react": "^2.4.2", |         "@chakra-ui/react": "^2.4.2", | ||||||
|         "@emotion/react": "^11.4.1", |         "@emotion/react": "^11.4.1", | ||||||
|         "@emotion/styled": "^11.3.0", |         "@emotion/styled": "^11.3.0", | ||||||
|  |         "@fontsource/open-sans": "^5.1.0", | ||||||
|         "@types/react": "^18.3.12", |         "@types/react": "^18.3.12", | ||||||
|         "express": "^4.21.1", |         "express": "^4.21.1", | ||||||
|         "framer-motion": "^6.2.8", |         "framer-motion": "^6.2.8", | ||||||
|         "react": "^18.3.1", |         "react": "^18.3.1", | ||||||
|         "react-dom": "^18.3.1", |         "react-dom": "^18.3.1", | ||||||
|  |         "react-icons": "^5.3.0", | ||||||
|         "react-router-dom": "^6.27.0" |         "react-router-dom": "^6.27.0" | ||||||
|     }, |     }, | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								remote-assets/demo.mp4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								remote-assets/demo.mp4
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										14
									
								
								src/assets/icons/dry-master-logo.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/assets/icons/dry-master-logo.svg
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										1
									
								
								src/assets/icons/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/assets/icons/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export { default as LogoSvg } from './dry-master-logo.svg'; | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/demo-video-poster.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/images/demo-video-poster.webp
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 51 KiB | 
							
								
								
									
										1
									
								
								src/assets/images/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/assets/images/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export { default as DemoVideoPosterImg } from './demo-video-poster.webp'; | ||||||
							
								
								
									
										16
									
								
								src/components/PageSpinner/PageSpinner.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/components/PageSpinner/PageSpinner.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | |||||||
|  | import React, { FC } from 'react'; | ||||||
|  | import { Flex, Spinner } from '@chakra-ui/react'; | ||||||
|  | 
 | ||||||
|  | export const PageSpinner: FC = () => { | ||||||
|  |   return ( | ||||||
|  |     <Flex w='full' h='100vh' justifyContent='center' alignItems='center'> | ||||||
|  |       <Spinner | ||||||
|  |         thickness='5px' | ||||||
|  |         speed='0.65s' | ||||||
|  |         emptyColor='gray.200' | ||||||
|  |         color='green.500' | ||||||
|  |         size='xl' | ||||||
|  |       /> | ||||||
|  |     </Flex> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								src/components/PageSpinner/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/PageSpinner/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export { PageSpinner } from './PageSpinner'; | ||||||
							
								
								
									
										1
									
								
								src/components/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export * from './PageSpinner'; | ||||||
							
								
								
									
										48
									
								
								src/components/landing/BenefitsSection/BenefitsSection.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/components/landing/BenefitsSection/BenefitsSection.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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 ( | ||||||
|  |     <PageSection> | ||||||
|  |       <VStack w='full' spacing={2}> | ||||||
|  |         <Heading as='h2'>Преимущества экологичной автомойки</Heading> | ||||||
|  |         <Text> | ||||||
|  |           Откройте для себя преимущества наших услуг по химчистке автомобилей | ||||||
|  |         </Text> | ||||||
|  |       </VStack> | ||||||
|  |       <List display='flex' flexDirection='column' spacing={3}> | ||||||
|  |         {[ | ||||||
|  |           { | ||||||
|  |             Icon: MdEco, | ||||||
|  |             children: 'Экологически безопасные продукты', | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             Icon: MdMiscellaneousServices, | ||||||
|  |             children: 'Быстрое и эффективное обслуживание', | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             Icon: MdPlace, | ||||||
|  |             children: 'Удобный мобильный доступ', | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             Icon: MdHandshake, | ||||||
|  |             children: 'Надежный и заслуживающий доверия', | ||||||
|  |           }, | ||||||
|  |         ].map((props, i) => ( | ||||||
|  |           <ListItem key={i} {...props} /> | ||||||
|  |         ))} | ||||||
|  |       </List> | ||||||
|  |       <HStack w='full' justify='flex-end'> | ||||||
|  |         <CtaButton /> | ||||||
|  |       </HStack> | ||||||
|  |     </PageSection> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										16
									
								
								src/components/landing/BenefitsSection/ListItem/ListItem.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/components/landing/BenefitsSection/ListItem/ListItem.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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<ListItemProps> = ({ Icon, children }) => { | ||||||
|  |   return ( | ||||||
|  |     <ChakraListItem display='inline-flex'> | ||||||
|  |       <ListIcon as={Icon} color='primary.500' boxSize='6' /> | ||||||
|  |       {children} | ||||||
|  |     </ChakraListItem> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								src/components/landing/BenefitsSection/ListItem/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/landing/BenefitsSection/ListItem/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export { ListItem } from './ListItem'; | ||||||
							
								
								
									
										1
									
								
								src/components/landing/BenefitsSection/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/landing/BenefitsSection/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export { BenefitsSection } from './BenefitsSection'; | ||||||
							
								
								
									
										11
									
								
								src/components/landing/CtaButton/CtaButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/components/landing/CtaButton/CtaButton.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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<ButtonProps> = (props) => { | ||||||
|  |   return ( | ||||||
|  |     <Button as={RouterLink} to='/dry-wash/order-form' colorScheme='primary' {...props}> | ||||||
|  |       Сделать заказ | ||||||
|  |     </Button> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								src/components/landing/CtaButton/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/landing/CtaButton/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export { CtaButton } from './CtaButton'; | ||||||
							
								
								
									
										8
									
								
								src/components/landing/Footer/Copyright/Copyright.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/components/landing/Footer/Copyright/Copyright.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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 <Text color='whiteAlpha.500'>© {currentYear} DryMaster. Все права защищены</Text>; | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								src/components/landing/Footer/Copyright/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/landing/Footer/Copyright/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export { Copyright } from './Copyright'; | ||||||
							
								
								
									
										27
									
								
								src/components/landing/Footer/Footer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/components/landing/Footer/Footer.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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 ( | ||||||
|  |     <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) => ( | ||||||
|  |           <ListItem key={i}> | ||||||
|  |             <Link as={RouterLink} to={to}> | ||||||
|  |               {label} | ||||||
|  |             </Link> | ||||||
|  |           </ListItem> | ||||||
|  |         ))} | ||||||
|  |       </List> | ||||||
|  |     </PageSection> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								src/components/landing/Footer/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/landing/Footer/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export { Footer } from './Footer'; | ||||||
							
								
								
									
										59
									
								
								src/components/landing/HeroSection/HeroSection.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/components/landing/HeroSection/HeroSection.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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<BoxProps, 'flexShrink'>; | ||||||
|  | 
 | ||||||
|  | export const HeroSection: FC<HeroSectionProps> = ({ flexShrink }) => { | ||||||
|  |   return ( | ||||||
|  |     <Box flexShrink={flexShrink} as='header' pos='relative' zIndex={0}> | ||||||
|  |       <Box | ||||||
|  |         as='video' | ||||||
|  |         src={`${__webpack_public_path__}/remote-assets/demo.mp4`} | ||||||
|  |         poster={DemoVideoPosterImg} | ||||||
|  |         autoPlay | ||||||
|  |         loop | ||||||
|  |         muted | ||||||
|  |         w='full' | ||||||
|  |         h='full' | ||||||
|  |         pos='absolute' | ||||||
|  |         objectFit='cover' | ||||||
|  |         filter='brightness(50%)' | ||||||
|  |         zIndex={-1} | ||||||
|  |       /> | ||||||
|  |       <PageSection | ||||||
|  |         h='full' | ||||||
|  |         minH='375px' | ||||||
|  |         maxH='1000px' | ||||||
|  |         py={10} | ||||||
|  |         justifyContent='center' | ||||||
|  |         alignItems='center' | ||||||
|  |         spacing={8} | ||||||
|  |       > | ||||||
|  |         <Center> | ||||||
|  |           <SiteLogo /> | ||||||
|  |         </Center> | ||||||
|  |         <VStack spacing={4}> | ||||||
|  |           <Heading | ||||||
|  |             as='h1' | ||||||
|  |             textAlign='center' | ||||||
|  |             color='white' | ||||||
|  |             __css={{ textWrap: 'balance' }} | ||||||
|  |           > | ||||||
|  |             Оживите свою поездку с помощью экологически чистого ухода! | ||||||
|  |           </Heading> | ||||||
|  |           <Text | ||||||
|  |             textAlign='center' | ||||||
|  |             __css={{ textWrap: 'balance' }} | ||||||
|  |             color='white' | ||||||
|  |           > | ||||||
|  |             Ощутите максимальное удобство сухой мойки автомобилей, созданной для | ||||||
|  |             того, чтобы планета стала чище | ||||||
|  |           </Text> | ||||||
|  |         </VStack> | ||||||
|  |         <CtaButton size='lg' /> | ||||||
|  |       </PageSection> | ||||||
|  |     </Box> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								src/components/landing/HeroSection/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/landing/HeroSection/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export { HeroSection } from './HeroSection'; | ||||||
							
								
								
									
										12
									
								
								src/components/landing/PageSection/PageSection.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/components/landing/PageSection/PageSection.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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<PageSectionProps> = ({ children, ...restProps }) => { | ||||||
|  |   return ( | ||||||
|  |     <VStack as='section' w='full' px={5} py={5} spacing={6} alignItems='flex-start' {...restProps}> | ||||||
|  |       {children} | ||||||
|  |     </VStack> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								src/components/landing/PageSection/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/landing/PageSection/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export { PageSection } from './PageSection'; | ||||||
							
								
								
									
										10
									
								
								src/components/landing/SiteLogo/SiteLogo.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/components/landing/SiteLogo/SiteLogo.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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 <Image src={LogoSvg} alt='Логотип компании "Сухой мастер"' w={40} />; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // todo: add i18n for alt
 | ||||||
|  | // todo: replace Image by SVG React component
 | ||||||
							
								
								
									
										1
									
								
								src/components/landing/SiteLogo/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/landing/SiteLogo/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export { SiteLogo } from './SiteLogo'; | ||||||
| @ -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<ReviewCardProps> = ({ | ||||||
|  |   firstname, | ||||||
|  |   lastname, | ||||||
|  |   picture, | ||||||
|  |   text, | ||||||
|  | }) => { | ||||||
|  |   const name = [firstname, lastname].join(' '); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <Card p={4} gap={2} alignItems='center' variant='elevated'> | ||||||
|  |       <Avatar | ||||||
|  |         name={name} | ||||||
|  |         src={picture} | ||||||
|  |         size='xl' | ||||||
|  |         boxShadow='2px 2px 0 1px var(--chakra-colors-secondary-500)' | ||||||
|  |       /> | ||||||
|  |       <Text | ||||||
|  |         as='q' | ||||||
|  |         fontSize='sm' | ||||||
|  |         textAlign='center' | ||||||
|  |         __css={{ textWrap: 'balance' }} | ||||||
|  |       > | ||||||
|  |         {text} | ||||||
|  |       </Text> | ||||||
|  |     </Card> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @ -0,0 +1 @@ | |||||||
|  | export { ReviewCard } from './ReviewCard'; | ||||||
| @ -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 ( | ||||||
|  |     <Tabs | ||||||
|  |       index={activeTab} | ||||||
|  |       onChange={(selectedTab) => { | ||||||
|  |         setIsSlideShowStopped(true); | ||||||
|  |         setActiveTab(selectedTab); | ||||||
|  |       }} | ||||||
|  |       display='flex' | ||||||
|  |       flexDir='column' | ||||||
|  |       alignItems='center' | ||||||
|  |       variant='soft-rounded' | ||||||
|  |       colorScheme='gray' | ||||||
|  |     > | ||||||
|  |       <TabList gap={2}> | ||||||
|  |         {mockReviews.map(({ id }, i) => ( | ||||||
|  |           <Tab | ||||||
|  |             key={id} | ||||||
|  |             w={4} | ||||||
|  |             h={4} | ||||||
|  |             p={0} | ||||||
|  |             bg='gray.100' | ||||||
|  |             _selected={{ | ||||||
|  |               bg: 'secondary.100', | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             <Text visibility='hidden'>{i}</Text> | ||||||
|  |           </Tab> | ||||||
|  |         ))} | ||||||
|  |       </TabList> | ||||||
|  |       <TabPanels> | ||||||
|  |         {mockReviews.map(({ id, ...reviewProps }) => ( | ||||||
|  |           <TabPanel key={id}> | ||||||
|  |             <ReviewCard {...reviewProps} /> | ||||||
|  |           </TabPanel> | ||||||
|  |         ))} | ||||||
|  |       </TabPanels> | ||||||
|  |     </Tabs> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @ -0,0 +1 @@ | |||||||
|  | export { ReviewsSlider } from './ReviewsSlider'; | ||||||
| @ -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 ( | ||||||
|  |     <PageSection> | ||||||
|  |       <Heading as='h2'>Нас выбирают</Heading> | ||||||
|  |       <ReviewsSlider /> | ||||||
|  |       <HStack w='full' justify='flex-end'> | ||||||
|  |         <CtaButton /> | ||||||
|  |       </HStack> | ||||||
|  |     </PageSection> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								src/components/landing/SocialProofSection/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/landing/SocialProofSection/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export { SocialProofSection } from './SocialProofSection'; | ||||||
							
								
								
									
										7
									
								
								src/components/landing/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/components/landing/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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
 | ||||||
							
								
								
									
										31
									
								
								src/containers/LandingThemeProvider/Fonts.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/containers/LandingThemeProvider/Fonts.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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 = () => ( | ||||||
|  |   <Global | ||||||
|  |     styles={` | ||||||
|  |       /* open-sans-cyrillic-400-normal */ | ||||||
|  |       @font-face { | ||||||
|  |         font-family: 'Open Sans'; | ||||||
|  |         font-style: normal; | ||||||
|  |         font-display: swap; | ||||||
|  |         font-weight: 400; | ||||||
|  |         src: url(./files/open-sans-cyrillic-400-normal.woff2) format('woff2'), url(./files/open-sans-cyrillic-400-normal.woff) format('woff'); | ||||||
|  |         unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; | ||||||
|  |       } | ||||||
|  |       /* open-sans-cyrillic-700-normal */ | ||||||
|  |       @font-face { | ||||||
|  |         font-family: 'Open Sans'; | ||||||
|  |         font-style: normal; | ||||||
|  |         font-display: swap; | ||||||
|  |         font-weight: 700; | ||||||
|  |         src: url(./files/open-sans-cyrillic-700-normal.woff2) format('woff2'), url(./files/open-sans-cyrillic-700-normal.woff) format('woff'); | ||||||
|  |         unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; | ||||||
|  |       } | ||||||
|  |       `}
 | ||||||
|  |   /> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export default Fonts; | ||||||
							
								
								
									
										13
									
								
								src/containers/LandingThemeProvider/LandingThemeProvider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/containers/LandingThemeProvider/LandingThemeProvider.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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<PropsWithChildren> = ({ children }) => { | ||||||
|  |   return ( | ||||||
|  |     <ChakraProvider theme={landingTheme}> | ||||||
|  |       <Fonts /> | ||||||
|  |       {children} | ||||||
|  |     </ChakraProvider> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								src/containers/LandingThemeProvider/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/containers/LandingThemeProvider/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export { LandingThemeProvider } from './LandingThemeProvider'; | ||||||
							
								
								
									
										42
									
								
								src/containers/LandingThemeProvider/theme-config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/containers/LandingThemeProvider/theme-config.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||||
							
								
								
									
										1
									
								
								src/containers/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/containers/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export * from './LandingThemeProvider'; | ||||||
							
								
								
									
										34
									
								
								src/mocks/landing/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/mocks/landing/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 })); | ||||||
| @ -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 = () => { | const Page: FC = () => { | ||||||
|   return <h1>Landing</h1>; |   return ( | ||||||
|  |     <LandingThemeProvider> | ||||||
|  |       <Container | ||||||
|  |         w='full' | ||||||
|  |         maxWidth='container.xl' | ||||||
|  |         minH='100vh' | ||||||
|  |         padding={0} | ||||||
|  |         bg='white' | ||||||
|  |         centerContent | ||||||
|  |       > | ||||||
|  |         <VStack w='full' h='full' alignItems='stretch'> | ||||||
|  |           <HeroSection flexShrink={0} /> | ||||||
|  |           <Box flexGrow={1}> | ||||||
|  |             <VStack as='main'> | ||||||
|  |               <BenefitsSection /> | ||||||
|  |               <SocialProofSection /> | ||||||
|  |             </VStack> | ||||||
|  |           </Box> | ||||||
|  |           <Footer /> | ||||||
|  |         </VStack> | ||||||
|  |       </Container> | ||||||
|  |     </LandingThemeProvider> | ||||||
|  |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default Page; | export default Page; | ||||||
|  | |||||||
| @ -1,16 +1,24 @@ | |||||||
| import React from 'react'; | 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 Arm from './pages/arm'; | import Arm from './pages/arm'; | ||||||
| import Order from './pages/order-view'; | 
 | ||||||
| import Landing from './pages/landing'; | const Landing = lazy(() => import('./pages/landing')); | ||||||
|  | const OrderForm = lazy(() => import('./pages/order-form')); | ||||||
|  | const OrderView = lazy(() => import('./pages/order-view')); | ||||||
| 
 | 
 | ||||||
| const Routers = () => { | const Routers = () => { | ||||||
|   return ( |   return ( | ||||||
|     <Routes> |     <Suspense fallback={<PageSpinner />}> | ||||||
|       <Route path='/dry-wash' element={<Landing />}></Route> |       <Routes> | ||||||
|       <Route path='/dry-wash/order' element={<Order />}></Route> |         <Route path='/dry-wash'> | ||||||
|       <Route path='/dry-wash/arm' element={<Arm />}></Route> |           <Route index element={<Landing />} /> | ||||||
|     </Routes> |           <Route path='order-form' element={<OrderForm />} /> | ||||||
|  |           <Route path='order-view' element={<OrderView />} /> | ||||||
|  |         </Route> | ||||||
|  |         <Route path='/dry-wash/arm' element={<Arm />}></Route> | ||||||
|  |       </Routes> | ||||||
|  |     </Suspense> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								types.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -3,3 +3,17 @@ declare interface NodeModule { | |||||||
|     accept: (path: string, callback: () => void) => void; |     accept: (path: string, callback: () => void) => void; | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | declare module "*.svg" { | ||||||
|  |   // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||||
|  |   const value: any; | ||||||
|  |   export = value; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | declare module "*.webp" { | ||||||
|  |   // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||||
|  |   const value: any; | ||||||
|  |   export = value; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | declare const __webpack_public_path__: string; | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user