add landing components
This commit is contained in:
parent
1c8054f7e8
commit
915e402647
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';
|
60
src/components/landing/HeroSection/HeroSection.tsx
Normal file
60
src/components/landing/HeroSection/HeroSection.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Box, Heading, Text, Center, VStack, BoxProps } from '@chakra-ui/react';
|
||||||
|
import { DemoVideo } from '../../../assets/videos';
|
||||||
|
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={DemoVideo}
|
||||||
|
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
|
Loading…
Reference in New Issue
Block a user