feat: landing (#7) #21

Merged
primakov merged 11 commits from feat/landing into main 2024-11-10 11:50:20 +03:00
23 changed files with 339 additions and 0 deletions
Showing only changes of commit 915e402647 - Show all commits

View 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>
);
};

View 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>
);
};

View File

@ -0,0 +1 @@
export { ListItem } from './ListItem';

View File

@ -0,0 +1 @@
export { BenefitsSection } from './BenefitsSection';

View 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>
);
};

View File

@ -0,0 +1 @@
export { CtaButton } from './CtaButton';

View 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'>&copy; {currentYear} DryMaster. Все права защищены</Text>;
};

View File

@ -0,0 +1 @@
export { Copyright } from './Copyright';

View 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>
);
};

View File

@ -0,0 +1 @@
export { Footer } from './Footer';

View 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>
);
};

View File

@ -0,0 +1 @@
export { HeroSection } from './HeroSection';

View 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>
);
};

View File

@ -0,0 +1 @@
export { PageSection } from './PageSection';

View 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

View File

@ -0,0 +1 @@
export { SiteLogo } from './SiteLogo';

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export { ReviewCard } from './ReviewCard';

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export { ReviewsSlider } from './ReviewsSlider';

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export { SocialProofSection } from './SocialProofSection';

View 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