feat: landing (#7) #21
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