Add docker
This commit is contained in:
58
src/modules/main/components/features.tsx
Normal file
58
src/modules/main/components/features.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Grid, Card, CardContent, Typography, Box, Container } from '@mui/material';
|
||||
|
||||
import SecurityIcon from '@mui/icons-material/Security';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import UpdateIcon from '@mui/icons-material/Update';
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <SecurityIcon fontSize="large" color="primary" />,
|
||||
title: 'Прозрачность и надёжность',
|
||||
text: 'Все представленные данные основаны на официальных источниках и актуальных предложениях банков.',
|
||||
},
|
||||
{
|
||||
icon: <AutoAwesomeIcon fontSize="large" color="primary" />,
|
||||
title: 'Современные технологии',
|
||||
text: 'Сайт создан с использованием современных веб-технологий для стабильной и быстрой работы.',
|
||||
},
|
||||
{
|
||||
icon: <SearchIcon fontSize="large" color="primary" />,
|
||||
title: 'Удобный поиск услуг',
|
||||
text: 'Интерактивный каталог позволяет быстро находить нужные продукты по типу, ставке или сроку.',
|
||||
},
|
||||
{
|
||||
icon: <UpdateIcon fontSize="large" color="primary" />,
|
||||
title: 'Актуальная информация',
|
||||
text: 'Информация регулярно обновляется, чтобы пользователи всегда получали свежие данные и выгодные предложения.',
|
||||
},
|
||||
];
|
||||
|
||||
export const Features = () => {
|
||||
return (
|
||||
<Container maxWidth="lg">
|
||||
<Box sx={{ py: 8 }}>
|
||||
<Typography component="h2" variant="h2" gutterBottom sx={{ color: 'text.primary', textAlign: 'center' }}>
|
||||
Наши преимущества
|
||||
</Typography>
|
||||
<Grid container spacing={4} sx={{ pt: 5 }}>
|
||||
{features.map((item, i) => (
|
||||
<Grid key={i} sx={{ display: 'flex' }} size={6}>
|
||||
<Card sx={{ height: '100%', textAlign: 'center', p: 2 }}>
|
||||
<Box sx={{ mt: 2 }}>{item.icon}</Box>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{item.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{item.text}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
18
src/modules/main/components/feedback.tsx
Normal file
18
src/modules/main/components/feedback.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Container, Box, Typography } from '@mui/material';
|
||||
import { FeedbackForm } from './form.tsx';
|
||||
|
||||
export const Feedback = () => {
|
||||
return (
|
||||
<Container id="contacts">
|
||||
<Box sx={{ pt: 2, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Typography component="h2" variant="h2" gutterBottom sx={{ color: 'text.primary', textAlign: 'center' }}>
|
||||
Свяжитесь с нами
|
||||
</Typography>
|
||||
<Typography component="h5" variant="h5" gutterBottom sx={{ color: 'text.primary', textAlign: 'center' }}>
|
||||
Мы поможем разобраться в услугах и ответим на все вопросы.
|
||||
</Typography>
|
||||
<FeedbackForm />
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
156
src/modules/main/components/form.tsx
Normal file
156
src/modules/main/components/form.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { type ChangeEvent, type FormEvent, useId, useState } from 'react';
|
||||
|
||||
import Alert from '@mui/material/Alert';
|
||||
import { Button, Stack, TextField } from '@mui/material';
|
||||
|
||||
import { leadsApi, type LeadCreatePayload } from '../../../api/index.ts';
|
||||
import { ApiError } from '../../../api/httpClient.ts';
|
||||
|
||||
type FeedbackFormFields = {
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
};
|
||||
|
||||
type FeedbackFormErrors = Record<keyof FeedbackFormFields, string | null>;
|
||||
|
||||
const defaultFormState: FeedbackFormFields = {
|
||||
fullName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
};
|
||||
|
||||
const createDefaultErrors = (): FeedbackFormErrors => ({
|
||||
fullName: null,
|
||||
email: null,
|
||||
phone: null,
|
||||
});
|
||||
|
||||
export const FeedbackForm = () => {
|
||||
const formInstanceId = useId();
|
||||
const [form, setForm] = useState<FeedbackFormFields>(defaultFormState);
|
||||
const [errors, setErrors] = useState<FeedbackFormErrors>(createDefaultErrors());
|
||||
const [alert, setAlert] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = event.target;
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[name]: null,
|
||||
}));
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
const nextErrors = createDefaultErrors();
|
||||
let isValid = true;
|
||||
|
||||
if (!form.fullName.trim()) {
|
||||
nextErrors.fullName = 'Укажите ваше имя';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!form.email.trim()) {
|
||||
nextErrors.email = 'Введите email';
|
||||
isValid = false;
|
||||
} else if (!/^[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}$/.test(form.email.trim())) {
|
||||
nextErrors.email = 'Введите корректный email';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (form.phone.trim() && form.phone.trim().length < 5) {
|
||||
nextErrors.phone = 'Введите телефон полностью';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
setErrors(nextErrors);
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setAlert(null);
|
||||
|
||||
if (!validate()) {
|
||||
setAlert({ type: 'error', message: 'Пожалуйста, проверьте правильность заполнения формы.' });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
const normalizedPhone = form.phone.trim();
|
||||
const payload: LeadCreatePayload = {
|
||||
fullName: form.fullName.trim(),
|
||||
email: form.email.trim(),
|
||||
...(normalizedPhone ? { phone: normalizedPhone } : {}),
|
||||
};
|
||||
|
||||
try {
|
||||
await leadsApi.create(payload);
|
||||
setAlert({ type: 'success', message: 'Спасибо! Мы свяжемся с вами в ближайшее время.' });
|
||||
setForm(defaultFormState);
|
||||
setErrors(createDefaultErrors());
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof ApiError ? err.message || 'Не удалось отправить заявку. Попробуйте ещё раз.' : 'Не удалось отправить заявку. Попробуйте ещё раз.';
|
||||
setAlert({ type: 'error', message });
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack component="form" onSubmit={handleSubmit} noValidate sx={{ alignItems: 'center', width: { xs: '100%', sm: '70%' } }} spacing={1.5}>
|
||||
{alert && <Alert severity={alert.type}>{alert.message}</Alert>}
|
||||
<TextField
|
||||
id={`${formInstanceId}-fullName`}
|
||||
name="fullName"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
placeholder="Иванов Иван"
|
||||
value={form.fullName}
|
||||
onChange={handleChange}
|
||||
error={Boolean(errors.fullName)}
|
||||
helperText={errors.fullName}
|
||||
fullWidth
|
||||
autoComplete="name"
|
||||
/>
|
||||
<Stack useFlexGap sx={{ alignItems: 'center', width: { xs: '100%' }, flexDirection: 'row' }} spacing={1}>
|
||||
<TextField
|
||||
id={`${formInstanceId}-email`}
|
||||
name="email"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
placeholder="email@example.com"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
error={Boolean(errors.email)}
|
||||
helperText={errors.email}
|
||||
fullWidth
|
||||
autoComplete="email"
|
||||
/>
|
||||
<TextField
|
||||
id={`${formInstanceId}-phone`}
|
||||
name="phone"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
placeholder="+7 999 123 45 67"
|
||||
value={form.phone}
|
||||
onChange={handleChange}
|
||||
error={Boolean(errors.phone)}
|
||||
helperText={errors.phone}
|
||||
fullWidth
|
||||
autoComplete="tel"
|
||||
inputProps={{ inputMode: 'tel' }}
|
||||
/>
|
||||
</Stack>
|
||||
<Button type="submit" variant="outlined" color="info" size="small" disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Отправляем...' : 'Оставить заявку'}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
52
src/modules/main/components/hero.tsx
Normal file
52
src/modules/main/components/hero.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Typography, Stack, Box, Container } from '@mui/material';
|
||||
import { FeedbackForm } from './form.tsx';
|
||||
|
||||
export const Hero = () => {
|
||||
return (
|
||||
<Box
|
||||
id="hero"
|
||||
sx={(theme) => ({
|
||||
width: '100%',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundImage: 'radial-gradient(ellipse 80% 50% at 50% -20%, hsl(210, 100%, 90%), transparent)',
|
||||
...theme.applyStyles('dark', {
|
||||
backgroundImage: 'radial-gradient(ellipse 80% 50% at 50% -20%, hsl(210, 100%, 16%), transparent)',
|
||||
}),
|
||||
})}
|
||||
>
|
||||
<Container
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
pt: { xs: 14, sm: 20 },
|
||||
pb: { xs: 8, sm: 12 },
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2} useFlexGap sx={{ alignItems: 'center', width: { xs: '100%', sm: '70%' } }}>
|
||||
<Typography
|
||||
variant="h1"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
alignItems: 'center',
|
||||
fontSize: 'clamp(3rem, 10vw, 3.5rem)',
|
||||
}}
|
||||
>
|
||||
Информационный портал о банковских услугах
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
color: 'text.secondary',
|
||||
width: { sm: '100%', md: '80%' },
|
||||
}}
|
||||
>
|
||||
Объединяем инновации и надёжность, чтобы финансовые решения стали простыми и доступными каждому.
|
||||
</Typography>
|
||||
<FeedbackForm />
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
102
src/modules/main/components/partners.tsx
Normal file
102
src/modules/main/components/partners.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Container, Box, Typography, Grid, Card, CardContent, Stack } from '@mui/material';
|
||||
|
||||
import Logo1 from '../../../assets/partners/logo1.png';
|
||||
import Logo2 from '../../../assets/partners/logo2.png';
|
||||
import Logo3 from '../../../assets/partners/logo3.png';
|
||||
import Logo4 from '../../../assets/partners/logo4.png';
|
||||
import Logo5 from '../../../assets/partners/logo5.png';
|
||||
import Logo6 from '../../../assets/partners/logo6.png';
|
||||
import Logo7 from '../../../assets/partners/logo7.png';
|
||||
import Logo8 from '../../../assets/partners/logo8.png';
|
||||
import Logo9 from '../../../assets/partners/logo9.png';
|
||||
import Logo10 from '../../../assets/partners/logo10.png';
|
||||
|
||||
export const Partners = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Box>
|
||||
<Typography component="h2" variant="h2" gutterBottom sx={{ color: 'text.primary', textAlign: 'center' }}>
|
||||
Наши Партнеры
|
||||
</Typography>
|
||||
<Stack direction="column" spacing={2} sx={{ width: '100%' }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={4}>
|
||||
<Card sx={{ textAlign: 'center', p: 2 }}>
|
||||
<CardContent>
|
||||
<img src={Logo1} style={{ width: '100%' }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Card sx={{ textAlign: 'center', p: 2 }}>
|
||||
<CardContent>
|
||||
<img src={Logo2} style={{ width: '100%' }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Card sx={{ textAlign: 'center', p: 2 }}>
|
||||
<CardContent>
|
||||
<img src={Logo3} style={{ width: '100%' }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={4}>
|
||||
<Card sx={{ textAlign: 'center', p: 2 }}>
|
||||
<CardContent>
|
||||
<img src={Logo4} style={{ width: '100%' }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Card sx={{ textAlign: 'center', p: 2 }}>
|
||||
<CardContent>
|
||||
<img src={Logo5} style={{ width: '100%' }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Card sx={{ textAlign: 'center', p: 2 }}>
|
||||
<CardContent>
|
||||
<img src={Logo6} style={{ width: '100%' }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={3}>
|
||||
<Card sx={{ textAlign: 'center', p: 2 }}>
|
||||
<CardContent>
|
||||
<img src={Logo7} style={{ width: '100%' }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Card sx={{ textAlign: 'center', p: 2 }}>
|
||||
<CardContent>
|
||||
<img src={Logo8} style={{ width: '100%' }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Card sx={{ textAlign: 'center', p: 2 }}>
|
||||
<CardContent>
|
||||
<img src={Logo9} style={{ width: '100%' }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Card sx={{ textAlign: 'center', p: 2 }}>
|
||||
<CardContent>
|
||||
<img src={Logo10} style={{ width: '100%' }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
90
src/modules/main/components/recent-news.tsx
Normal file
90
src/modules/main/components/recent-news.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Container from '@mui/material/Container';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { useStore } from '../../../shared/hooks/useStore.ts';
|
||||
import { formatDate, getDateValue } from '../../news/utils/formatDate.ts';
|
||||
|
||||
const RecentNewsComponent: React.FC = () => {
|
||||
const { news } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
await news.fetch({ limit: 10 });
|
||||
} catch {
|
||||
/* errors surfaced via store state */
|
||||
}
|
||||
};
|
||||
|
||||
void load();
|
||||
}, [news]);
|
||||
|
||||
const visibleNews = news.list
|
||||
.slice()
|
||||
.sort((a, b) => getDateValue(b.publishedAt) - getDateValue(a.publishedAt))
|
||||
.slice(0, 3);
|
||||
|
||||
const hasNews = visibleNews.length > 0;
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg">
|
||||
<Box sx={{ display: { xs: 'none', md: 'flex', width: '100%' }, mt: '2rem' }}>
|
||||
<Stack direction="column" spacing={2} sx={{ width: '100%' }}>
|
||||
<Typography component="h2" variant="h2" gutterBottom sx={{ color: 'text.primary', textAlign: 'center' }}>
|
||||
Последние новости
|
||||
</Typography>
|
||||
{news.isLoading && (
|
||||
<Typography sx={{ textAlign: 'center' }} color="text.secondary">
|
||||
Загружаем последние новости...
|
||||
</Typography>
|
||||
)}
|
||||
{news.error && (
|
||||
<Typography sx={{ textAlign: 'center' }} color="error">
|
||||
{news.error}
|
||||
</Typography>
|
||||
)}
|
||||
{hasNews && (
|
||||
<Grid container spacing={2}>
|
||||
{visibleNews.map((item) => (
|
||||
<Grid key={item.id} size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<Card
|
||||
key={item.id}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<CardHeader title={item.title} subheader={formatDate(item.publishedAt)} />
|
||||
<CardContent>
|
||||
<Typography gutterBottom>{item.summary}</Typography>
|
||||
<Link href={item.href}>Читать полностью</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
<Link href="/news" sx={{ alignSelf: 'center' }}>
|
||||
Лента новостей
|
||||
</Link>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export const RecentNews = observer(RecentNewsComponent);
|
||||
98
src/modules/main/components/services.tsx
Normal file
98
src/modules/main/components/services.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Container from '@mui/material/Container';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { useStore } from '../../../shared/hooks/useStore.ts';
|
||||
import { formatPrice } from '../../services/utils/formatPrice.ts';
|
||||
|
||||
const ServicesComponent: React.FC = () => {
|
||||
const { services } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
await services.fetch({ limit: 10 });
|
||||
} catch {
|
||||
/* handled via store state */
|
||||
}
|
||||
};
|
||||
|
||||
void load();
|
||||
}, [services]);
|
||||
|
||||
const visibleServices = services.list
|
||||
.slice()
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
.slice(0, 3);
|
||||
|
||||
const hasServices = visibleServices.length > 0;
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg">
|
||||
<Box sx={{ display: 'flex', width: '100%' }}>
|
||||
<Stack direction="column" spacing={2} sx={{ width: '100%' }}>
|
||||
<Typography id="services_anchor" component="h2" variant="h2" gutterBottom sx={{ color: 'text.primary', textAlign: 'center' }}>
|
||||
Наши услуги
|
||||
</Typography>
|
||||
{services.isLoading && (
|
||||
<Typography sx={{ textAlign: 'center' }} color="text.secondary">
|
||||
Загружаем услуги...
|
||||
</Typography>
|
||||
)}
|
||||
{services.error && (
|
||||
<Typography sx={{ textAlign: 'center' }} color="error">
|
||||
{services.error}
|
||||
</Typography>
|
||||
)}
|
||||
{hasServices && (
|
||||
<Grid container spacing={2}>
|
||||
{visibleServices.map((service) => (
|
||||
<Grid key={service.id} size={{ xs: 12, md: 6, lg: 4 }}>
|
||||
<Card
|
||||
variant="outlined"
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<CardHeader title={service.title} subheader={service.category?.name ?? 'Без категории'} sx={{ pb: 0 }} />
|
||||
<CardContent>
|
||||
<Typography variant="caption" color="text.secondary" display="block" sx={{ mb: 1 }}>
|
||||
{formatPrice(service.priceFrom)}
|
||||
</Typography>
|
||||
<Typography gutterBottom color="text.secondary">
|
||||
{service.description}
|
||||
</Typography>
|
||||
<Link href={service.href}>Подробнее</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
{!services.isLoading && !services.error && !hasServices && (
|
||||
<Typography sx={{ textAlign: 'center' }} color="text.secondary">
|
||||
Услуги скоро появятся.
|
||||
</Typography>
|
||||
)}
|
||||
<Link href={'/services'} sx={{ alignSelf: 'center' }}>
|
||||
Все услуги
|
||||
</Link>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export const Services = observer(ServicesComponent);
|
||||
Reference in New Issue
Block a user