Centralize and update contact information across app

Introduced a centralized contacts configuration in src/config/contacts.js, added a useContacts hook, and a reusable ContactInfo component. Updated Header, Footer, Home, Office, Services, About, Objects, and Apartament pages to use the new contact data source. Added documentation in CONTACTS_CONFIG.md and included the AlmaVid logo asset.
This commit is contained in:
Madara0330E
2025-07-16 23:16:00 +05:00
parent 2af795f819
commit 4adbf791ea
20 changed files with 2007 additions and 838 deletions

122
CONTACTS_CONFIG.md Normal file
View File

@@ -0,0 +1,122 @@
# Конфигурация контактной информации
Этот файл содержит всю контактную информацию агентства недвижимости АЛМА-ВИД в централизованном виде.
## Расположение файла
`src/config/contacts.js`
## Структура конфигурации
### Основные контакты
- `phone` - номер телефона с отображаемым текстом и ссылкой для набора
- `email` - электронная почта с отображаемым текстом и ссылкой mailto
### Адреса
- `address.main` - основной адрес офиса (для карт и контактов)
- `address.office` - рабочий адрес офиса (для страницы "Наш офис")
### Координаты
- `coordinates` - координаты для отображения на Яндекс.Картах
### Социальные сети
- `social.vk` - ВКонтакте
- `social.telegram` - Telegram
- `social.instagram` - Instagram
### Название компании
- `companyName` - полное название
- `companyNameShort` - сокращенное название
## Использование
### В компонентах
```jsx
import { CONTACTS } from '../../config/contacts';
// Использование телефона
<a href={CONTACTS.phone.link}>{CONTACTS.phone.display}</a>
// Использование email
<a href={CONTACTS.email.link}>{CONTACTS.email.display}</a>
// Использование соцсетей
<a href={CONTACTS.social.vk.url}>{CONTACTS.social.vk.name}</a>
// Координаты для карт
<Map defaultState={{ center: CONTACTS.coordinates, zoom: 12 }}>
<Placemark
geometry={CONTACTS.coordinates}
properties={{
balloonContent: CONTACTS.getBalloonContent(),
hintContent: `Офис ${CONTACTS.companyNameShort}`
}}
/>
</Map>
```
### С помощью хука useContacts
```jsx
import { useContacts } from "../../hooks/useContacts";
function MyComponent() {
const contacts = useContacts();
return (
<div>
<a href={contacts.getPhoneLink()}>{contacts.phone.display}</a>
<a href={contacts.getSocialLink("vk")}>{contacts.getSocialName("vk")}</a>
</div>
);
}
```
### Компонент ContactInfo
```jsx
import { ContactInfo } from '../../components/ContactInfo/ContactInfo';
// Базовый вариант
<ContactInfo />
// Inline вариант
<ContactInfo variant="inline" />
// Полный вариант со всей информацией
<ContactInfo variant="full" />
```
## Обновленные страницы
Все следующие страницы теперь используют централизованную конфигурацию:
1. **Header** - номер телефона и социальные сети
2. **Footer** - социальные сети
3. **Home** - заголовок страницы, карта с координатами и balloon
4. **Office** - заголовок страницы, контактная информация, карта
5. **Services** - заголовок страницы, ссылка на ВКонтакте
6. **About** - заголовок страницы, название компании
7. **Objects** - заголовок страницы
8. **Apartament** - карта с координатами и balloon
## Преимущества централизованной конфигурации
1. **Единое место для изменений** - все контакты изменяются в одном файле
2. **Консистентность** - одинаковое отображение на всех страницах
3. **Легкость обслуживания** - не нужно искать и изменять контакты по всему проекту
4. **Типизация** - структурированные данные с понятным API
5. **Переиспользование** - легко добавлять контакты на новые страницы
## Как изменить контактную информацию
1. Откройте файл `src/config/contacts.js`
2. Измените нужные значения
3. Сохраните файл
4. Изменения автоматически применятся ко всем страницам

8
package-lock.json generated
View File

@@ -17219,16 +17219,16 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.8.3", "version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"peer": true, "peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
}, },
"engines": { "engines": {
"node": ">=14.17" "node": ">=4.2.0"
} }
}, },
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -0,0 +1,78 @@
import { CONTACTS } from "../../config/contacts";
import "./ContactInfo.scss";
function ContactInfo({ variant = "default" }) {
if (variant === "inline") {
return (
<div className="contact-info-inline">
<span className="contact-info__phone">
<a href={CONTACTS.phone.link}>{CONTACTS.phone.display}</a>
</span>
<span className="contact-info__email">
<a href={CONTACTS.email.link}>{CONTACTS.email.display}</a>
</span>
</div>
);
}
if (variant === "full") {
return (
<div className="contact-info-full">
<div className="contact-info__item">
<span className="contact-info__label">Телефон:</span>
<a href={CONTACTS.phone.link} className="contact-info__value">
{CONTACTS.phone.display}
</a>
</div>
<div className="contact-info__item">
<span className="contact-info__label">E-mail:</span>
<a href={CONTACTS.email.link} className="contact-info__value">
{CONTACTS.email.display}
</a>
</div>
<div className="contact-info__item">
<span className="contact-info__label">Адрес:</span>
<span className="contact-info__value">
{CONTACTS.address.main.full}
</span>
</div>
<div className="contact-info__item">
<span className="contact-info__label">Соц.сети:</span>
<div className="contact-info__social">
<a
href={CONTACTS.social.vk.url}
className="contact-info__social-link"
>
{CONTACTS.social.vk.name}
</a>
<a
href={CONTACTS.social.telegram.url}
className="contact-info__social-link"
>
{CONTACTS.social.telegram.name}
</a>
<a
href={CONTACTS.social.instagram.url}
className="contact-info__social-link"
>
{CONTACTS.social.instagram.name}
</a>
</div>
</div>
</div>
);
}
return (
<div className="contact-info">
<a href={CONTACTS.phone.link} className="contact-info__phone">
{CONTACTS.phone.display}
</a>
<a href={CONTACTS.email.link} className="contact-info__email">
{CONTACTS.email.display}
</a>
</div>
);
}
export { ContactInfo };

View File

@@ -0,0 +1,72 @@
.contact-info {
display: flex;
gap: 1rem;
align-items: center;
&__phone,
&__email {
text-decoration: none;
color: inherit;
font-weight: 600;
transition: color 0.3s ease;
&:hover {
color: #007bff;
}
}
}
.contact-info-inline {
display: flex;
gap: 2rem;
align-items: center;
.contact-info__phone,
.contact-info__email {
a {
text-decoration: none;
color: inherit;
font-weight: 600;
transition: color 0.3s ease;
&:hover {
color: #007bff;
}
}
}
}
.contact-info-full {
.contact-info__item {
margin-bottom: 1rem;
.contact-info__label {
font-weight: bold;
margin-right: 0.5rem;
}
.contact-info__value {
text-decoration: none;
color: inherit;
&:hover {
color: #007bff;
}
}
}
.contact-info__social {
display: flex;
gap: 1rem;
&-link {
text-decoration: none;
color: inherit;
transition: color 0.3s ease;
&:hover {
color: #007bff;
}
}
}
}

View File

@@ -1,5 +1,6 @@
import './Footer.scss'; import "./Footer.scss";
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from "react-router-dom";
import { CONTACTS } from "../../config/contacts";
function Footer() { function Footer() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -7,7 +8,7 @@ function Footer() {
const handleClick = () => { const handleClick = () => {
window.scrollTo({ window.scrollTo({
top: 0, top: 0,
behavior: 'smooth' behavior: "smooth",
}); });
}; };
@@ -18,18 +19,37 @@ function Footer() {
return ( return (
<footer className="footer"> <footer className="footer">
<Link to="/" className="footer__logo font-inter-regular" <Link
onClick={() => handlePageChange('/')}>OOO "ALMA-VID"</Link> to="/"
className="footer__logo font-inter-regular"
onClick={() => handlePageChange("/")}
>
OOO "ALMA-VID"
</Link>
<div className="footer-info font-inter-bold"> <div className="footer-info font-inter-bold">
<Link to="/objects" onClick={() => handlePageChange('/objects')}>Объекты</Link> <Link to="/objects" onClick={() => handlePageChange("/objects")}>
<Link to="/services" onClick={() => handlePageChange('/services')}>Услуги</Link> Объекты
<Link to="/about" onClick={() => handlePageChange('/about')}>О компании</Link> </Link>
<Link to="/office" onClick={() => handlePageChange('/office')}>Контакты</Link> <Link to="/services" onClick={() => handlePageChange("/services")}>
Услуги
</Link>
<Link to="/about" onClick={() => handlePageChange("/about")}>
О компании
</Link>
<Link to="/office" onClick={() => handlePageChange("/office")}>
Контакты
</Link>
</div> </div>
<div className="footer-socials"> <div className="footer-socials">
<a className="footer-socials__tg" href="#"></a> <a
<a className="footer-socials__inst" href="#"></a> className="footer-socials__tg"
<a className="footer-socials__vk" href="#"></a> href={CONTACTS.social.telegram.url}
></a>
<a
className="footer-socials__inst"
href={CONTACTS.social.instagram.url}
></a>
<a className="footer-socials__vk" href={CONTACTS.social.vk.url}></a>
</div> </div>
</footer> </footer>
); );

View File

@@ -1,16 +1,20 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import './Form.scss'; import "./Form.scss";
import PencilIcon from '../../assets/images/icons/pencil.svg'; import PencilIcon from "../../assets/images/icons/pencil.svg";
import { API_CONFIG } from "../../config/contacts";
function Form({ scrolledThreshold }) { function Form({ scrolledThreshold }) {
const [name, setName] = useState(''); const [name, setName] = useState("");
const [phone, setPhone] = useState(''); const [phone, setPhone] = useState("");
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [submitStatus, setSubmitStatus] = useState(null); // 'success', 'error', null
const checkVisible = () => { const checkVisible = () => {
const scrolled = document.documentElement.scrollTop; const scrolled = document.documentElement.scrollTop;
if (scrolled > scrolledThreshold){ // вы можете настроить это значение if (scrolled > scrolledThreshold) {
// вы можете настроить это значение
if (!isVisible) setVisible(true); if (!isVisible) setVisible(true);
} else { } else {
if (isVisible) setVisible(false); if (isVisible) setVisible(false);
@@ -18,22 +22,100 @@ function Form({ scrolledThreshold }) {
}; };
useEffect(() => { useEffect(() => {
window.addEventListener('scroll', checkVisible); window.addEventListener("scroll", checkVisible);
return () => window.removeEventListener('scroll', checkVisible); return () => window.removeEventListener("scroll", checkVisible);
}); });
const handleSubmit = (e) => { // Автоматически скрываем сообщение об успехе через 5 секунд
useEffect(() => {
if (submitStatus === "success") {
const timer = setTimeout(() => {
setSubmitStatus(null);
}, 5000);
return () => clearTimeout(timer);
}
}, [submitStatus]);
// Функция для форматирования номера телефона
const formatPhoneNumber = (value) => {
// Убираем все нецифровые символы
const phoneNumber = value.replace(/\D/g, "");
// Ограничиваем длину номера
if (phoneNumber.length > 11) {
return phone; // возвращаем предыдущее значение, если превышена длина
}
// Форматируем номер
if (phoneNumber.length === 0) return "";
if (phoneNumber.length <= 1) return `+7`;
if (phoneNumber.length <= 4) return `+7(${phoneNumber.slice(1)}`;
if (phoneNumber.length <= 7)
return `+7(${phoneNumber.slice(1, 4)})${phoneNumber.slice(4)}`;
if (phoneNumber.length <= 9)
return `+7(${phoneNumber.slice(1, 4)})${phoneNumber.slice(
4,
7
)}-${phoneNumber.slice(7)}`;
return `+7(${phoneNumber.slice(1, 4)})${phoneNumber.slice(
4,
7
)}-${phoneNumber.slice(7, 9)}-${phoneNumber.slice(9, 11)}`;
};
const handlePhoneChange = (e) => {
const formattedPhone = formatPhoneNumber(e.target.value);
setPhone(formattedPhone);
};
const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
// в этой функции можно добавить логику для отправки формы на сервер
console.log(`${name}, ваш номер телефона: ${phone}`); // Проверяем, что поля заполнены
setName(''); if (!name.trim() || !phone.trim()) {
setPhone(''); setSubmitStatus("error");
return;
}
setIsLoading(true);
setSubmitStatus(null);
try {
const response = await fetch(
API_CONFIG.getFullURL(API_CONFIG.endpoints.feedback),
{
method: "POST",
headers: {
"Content-Type": "application/json",
accept: "*/*",
},
body: JSON.stringify({
firstName: name.trim(),
phoneNumber: phone.trim().replace(/\D/g, ""), // убираем все нецифровые символы
}),
}
);
if (response.ok) {
setSubmitStatus("success");
setName("");
setPhone("");
console.log("Заявка успешно отправлена");
} else {
throw new Error("Ошибка при отправке заявки");
}
} catch (error) {
console.error("Ошибка при отправке формы:", error);
setSubmitStatus("error");
} finally {
setIsLoading(false);
}
}; };
function scrollToTop() { function scrollToTop() {
window.scrollTo({ window.scrollTo({
top: 0, top: 0,
behavior: 'smooth' behavior: "smooth",
}); });
} }
@@ -47,6 +129,39 @@ function Form({ scrolledThreshold }) {
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<p className="form__title">Заполните форму ниже</p> <p className="form__title">Заполните форму ниже</p>
<p>Мы позвоним вам в ближайшее время</p> <p>Мы позвоним вам в ближайшее время</p>
{submitStatus === "success" && (
<div
style={{
color: "green",
marginBottom: "15px",
padding: "10px",
backgroundColor: "#d4edda",
border: "1px solid #c3e6cb",
borderRadius: "4px",
}}
>
Спасибо! Ваша заявка отправлена. Мы свяжемся с вами в ближайшее
время.
</div>
)}
{submitStatus === "error" && (
<div
style={{
color: "red",
marginBottom: "15px",
padding: "10px",
backgroundColor: "#f8d7da",
border: "1px solid #f5c6cb",
borderRadius: "4px",
}}
>
Ошибка при отправке. Пожалуйста, проверьте данные и попробуйте
снова.
</div>
)}
<div> <div>
<input <input
className="font-inter-regular" className="font-inter-regular"
@@ -54,28 +169,37 @@ function Form({ scrolledThreshold }) {
value={name} value={name}
placeholder="Ваше имя" placeholder="Ваше имя"
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
required
disabled={isLoading}
/> />
<input <input
className="font-inter-regular" className="font-inter-regular"
type="tel" type="tel"
value={phone} value={phone}
placeholder="+7(800)555-35-35" placeholder="+7(800)555-35-35"
onChange={(e) => setPhone(e.target.value)} onChange={handlePhoneChange}
required
disabled={isLoading}
/> />
<button <button
className="form-btn font-inter-regular" className="form-btn font-inter-regular"
type="submit" type="submit"
disabled={isLoading || !name.trim() || !phone.trim()}
style={{
opacity: isLoading ? 0.7 : 1,
cursor: isLoading ? "not-allowed" : "pointer",
}}
> >
Записаться на консультацию {isLoading ? "Отправляем..." : "Записаться на консультацию"}
</button> </button>
</div> </div>
<p> <p>Заполняя форму, вы соглашаетесь с политикой конфиденциальности</p>
Заполняя форму, вы соглашаетесь с политикой
конфиденциальности
</p>
</form> </form>
<div className="que-form"> <div className="que-form">
<div className={`arrow ${isVisible ? 'arrow__visible' : ''}`} onClick={scrollToTop}></div> <div
className={`arrow ${isVisible ? "arrow__visible" : ""}`}
onClick={scrollToTop}
></div>
<p className="font-inter-bold"> <p className="font-inter-bold">
У вас остались вопросы? <span>Напишите нам, мы онлайн!</span> У вас остались вопросы? <span>Напишите нам, мы онлайн!</span>
</p> </p>

View File

@@ -1,6 +1,7 @@
import './Header.scss'; import "./Header.scss";
import React, { useState } from 'react'; import React, { useState } from "react";
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from "react-router-dom";
import { CONTACTS } from "../../config/contacts";
function Header() { function Header() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -10,17 +11,28 @@ function Header() {
}; };
const location = useLocation(); const location = useLocation();
const isGradientBackground = location.pathname === '/apartament'; const isGradientBackground = location.pathname === "/apartament";
const isHomePage = location.pathname === '/'; const isHomePage = location.pathname === "/";
return ( return (
<header className={`header font-inter-bold ${isGradientBackground ? 'header_gradient' : ''}`}> <header
className={`header font-inter-bold ${
isGradientBackground ? "header_gradient" : ""
}`}
>
<Link className="header__logo" to="/"></Link> <Link className="header__logo" to="/"></Link>
{isHomePage && <div className="header__text"></div>} {isHomePage && <div className="header__text"></div>}
<div className="burgerMenu"> <div className="burgerMenu">
<a href="tel:+73512170074" className="header-info__call">8(351)217-00-74</a> <a href={CONTACTS.phone.link} className="header-info__call">
{CONTACTS.phone.display}
</a>
<div className="burger-item"> <div className="burger-item">
<input id="toggle" type="checkbox" checked={isOpen} onChange={handleToggle}></input> <input
<label for="toggle" className={`hamburger ${isOpen ? 'open' : ''}`}> id="toggle"
type="checkbox"
checked={isOpen}
onChange={handleToggle}
></input>
<label for="toggle" className={`hamburger ${isOpen ? "open" : ""}`}>
<div class="top-bun"></div> <div class="top-bun"></div>
<div class="meat"></div> <div class="meat"></div>
<div class="bottom-bun"></div> <div class="bottom-bun"></div>
@@ -40,11 +52,20 @@ function Header() {
<Link to="/office" className="header__link" href="#"> <Link to="/office" className="header__link" href="#">
Наш офис Наш офис
</Link> </Link>
<p>Челябинск</p> <p>{CONTACTS.address.main.city}</p>
<div className="header-socials__links"> <div className="header-socials__links">
<a className="header-socials__tg" href="#"></a> <a
<a className="header-socials__inst" href="#"></a> className="header-socials__tg"
<a className="header-socials__vk" href="#"></a> href={CONTACTS.social.telegram.url}
></a>
<a
className="header-socials__inst"
href={CONTACTS.social.instagram.url}
></a>
<a
className="header-socials__vk"
href={CONTACTS.social.vk.url}
></a>
</div> </div>
</nav> </nav>
</div> </div>
@@ -66,17 +87,27 @@ function Header() {
</Link> </Link>
</nav> </nav>
<div className="header-info"> <div className="header-info">
<a href="tel:+73512170074" className="header-info__call">8(351)217-00-74</a> <a href={CONTACTS.phone.link} className="header-info__call">
{CONTACTS.phone.display}
</a>
<button className="header-info__btn" type="button"> <button className="header-info__btn" type="button">
Обратный звонок Обратный звонок
</button> </button>
</div> </div>
<div className="header-socials"> <div className="header-socials">
<span className="header-socials__location">Челябинск</span> <span className="header-socials__location">
{CONTACTS.address.main.city}
</span>
<div className="header-socials__links"> <div className="header-socials__links">
<a className="header-socials__tg" href="#"></a> <a
<a className="header-socials__inst" href="#"></a> className="header-socials__tg"
<a className="header-socials__vk" href="#"></a> href={CONTACTS.social.telegram.url}
></a>
<a
className="header-socials__inst"
href={CONTACTS.social.instagram.url}
></a>
<a className="header-socials__vk" href={CONTACTS.social.vk.url}></a>
</div> </div>
</div> </div>
</header> </header>

View File

@@ -1,4 +1,4 @@
@import '../../styles/vars.scss'; @import "../../styles/vars.scss";
a { a {
color: white; color: white;
@@ -18,13 +18,13 @@ a {
} }
.header_gradient { .header_gradient {
background: linear-gradient(180deg, #061A25 0%, rgba(18, 114, 170, 0.7) 150%); background: linear-gradient(180deg, #061a25 0%, rgba(18, 114, 170, 0.7) 150%);
} }
.header__logo { .header__logo {
grid-row: 1/3; grid-row: 1/3;
background-image: url('../../assets/images/logo/logo-png.png'); background-image: url("../../assets/images/logo/AlmaVid-Logo.svg");
width: 302px; width: 320px;
height: 69px; height: 69px;
background-size: cover; background-size: cover;
} }
@@ -103,7 +103,7 @@ a {
@media (min-width: 1800px) { @media (min-width: 1800px) {
.header__logo { .header__logo {
width: 372px; width: 400px;
height: 85px; height: 85px;
} }
@@ -142,13 +142,12 @@ a {
} }
@media (max-width: $desktopWidth) { @media (max-width: $desktopWidth) {
.header { .header {
padding: 36px; padding: 36px;
} }
.header__logo { .header__logo {
width: 270px; width: 290px;
height: 62px; height: 62px;
} }
@@ -184,7 +183,7 @@ a {
} }
.header__logo { .header__logo {
width: 200px; width: 220px;
height: 46px; height: 46px;
} }
@@ -201,7 +200,12 @@ a {
font-size: 11px; font-size: 11px;
} }
} }
@media (max-width: 780px) {
.header__logo {
width: 210px;
height: 42px;
}
}
h1 { h1 {
text-align: center; text-align: center;
letter-spacing: 1px; letter-spacing: 1px;
@@ -211,7 +215,6 @@ h1 {
transform: translateY(52%); transform: translateY(52%);
} }
#toggle { #toggle {
display: none; display: none;
} }
@@ -251,7 +254,7 @@ Nav Styles
position: fixed; position: fixed;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: #17628C; background-color: #17628c;
top: -100%; top: -100%;
left: 0; left: 0;
right: 0; right: 0;
@@ -295,7 +298,7 @@ Nav Styles
} }
.nav-container .header__link:before { .nav-container .header__link:before {
content: ''; content: "";
height: 0; height: 0;
position: absolute; position: absolute;
width: 0.25em; width: 0.25em;
@@ -363,7 +366,7 @@ Animations
.header__logo { .header__logo {
// grid-row: 1/3; // grid-row: 1/3;
background-image: url('../../assets/images/logo/AlvaMid-Logo-Small.svg'); background-image: url("../../assets/images/logo/AlvaMid-Logo-Small.svg");
width: 50px; width: 50px;
height: 50px; height: 50px;
} }
@@ -376,7 +379,7 @@ Animations
.header__text { .header__text {
position: absolute; position: absolute;
background-image: url('../../assets/images/logo/textLogo.svg'); background-image: url("../../assets/images/logo/textLogo.svg");
top: 40%; top: 40%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);

View File

@@ -66,6 +66,55 @@
.item__image { .item__image {
border-top-right-radius: 15px; border-top-right-radius: 15px;
border-top-left-radius: 15px; border-top-left-radius: 15px;
width: 100%;
height: 250px;
object-fit: cover;
object-position: center;
display: block;
@media (max-width: 1300px) {
height: 230px;
}
@media (max-width: 1200px) {
height: 210px;
}
@media (max-width: 1100px) {
height: 190px;
}
@media (max-width: $laptopWidth) {
height: 170px;
}
@media (max-width: $tabletWidth) {
height: 300px;
}
@media (max-width: 740px) {
height: 280px;
}
@media (max-width: 690px) {
height: 250px;
}
@media (max-width: 635px) {
height: 220px;
}
@media (max-width: 570px) {
height: 190px;
}
@media (max-width: $mobileWidth) {
height: 210px;
}
@media (max-width: 420px) {
height: 190px;
}
} }
.item__info { .item__info {

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import Slider from 'react-slick'; import Slider from 'react-slick';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useMediaQuery } from 'react-responsive'; import { useMediaQuery } from 'react-responsive';
@@ -7,6 +7,7 @@ import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css'; import 'slick-carousel/slick/slick-theme.css';
import './SliderObjects.scss'; import './SliderObjects.scss';
import { Object } from '../Object/Object'; import { Object } from '../Object/Object';
import { API_CONFIG } from '../../config/contacts';
import objectPicOne from '../../assets/images/apartaments/image-44.jpg'; import objectPicOne from '../../assets/images/apartaments/image-44.jpg';
import objectPicTwo from '../../assets/images/apartaments/image-45.jpg'; import objectPicTwo from '../../assets/images/apartaments/image-45.jpg';
@@ -25,10 +26,40 @@ const PrevArrow = ({ onClick }) => {
}; };
const SliderComponent = () => { const SliderComponent = () => {
const [objects, setObjects] = useState([]);
const [loading, setLoading] = useState(true);
const isMobileResolution = useMediaQuery({ maxWidth: 768 }); const isMobileResolution = useMediaQuery({ maxWidth: 768 });
// Загрузка данных с API
useEffect(() => {
const fetchObjects = async () => {
try {
const response = await fetch(API_CONFIG.getFullURL(API_CONFIG.endpoints.rental), {
method: 'GET',
headers: {
'accept': '*/*'
}
});
if (response.ok) {
const data = await response.json();
setObjects(data);
}
} catch (error) {
console.error('Ошибка при загрузке объектов:', error);
// В случае ошибки оставляем пустой массив, компонент не сломается
} finally {
setLoading(false);
}
};
fetchObjects();
}, []);
const settings = { const settings = {
dots: false, dots: objects.length > (isMobileResolution ? 1 : 3), // Показываем точки если объектов больше чем влезает
infinite: true, infinite: objects.length > (isMobileResolution ? 1 : 3), // Бесконечная прокрутка только если объектов достаточно
speed: 500, speed: 500,
slidesToShow: isMobileResolution ? 1 : 3, slidesToShow: isMobileResolution ? 1 : 3,
slidesToScroll: isMobileResolution ? 1 : 3, slidesToScroll: isMobileResolution ? 1 : 3,
@@ -37,34 +68,19 @@ const SliderComponent = () => {
prevArrow: <PrevArrow />, prevArrow: <PrevArrow />,
}; };
// Если загружаем данные, показываем статические объекты как fallback
if (loading || objects.length === 0) {
return ( return (
<div className="slider-objects"> <div className="slider-objects">
<Slider {...settings}> <Slider {...{...settings, dots: false, infinite: true}}>
<Link className="objects-link" to="/apartament"> <Link className="objects-link" to="/apartament">
<Object
image={objectPicOne}
price="1 234 567 ₽"
desc="1-комн. кв. 34 м"
address="Ул. Луначарского, Ленинский район"
/></Link>
<Object
image={objectPicTwo}
price="1 234 567 ₽"
desc="1-комн. кв. 34 м"
address="Ул. Зари, Вагонка"
/>
<Object
image={objectPicThree}
price="1 234 567 ₽"
desc="1-комн. кв. 34 м"
address="Ул. Солнечная, Заречный район"
/>
<Object <Object
image={objectPicOne} image={objectPicOne}
price="1 234 567 ₽" price="1 234 567 ₽"
desc="1-комн. кв. 34 м" desc="1-комн. кв. 34 м"
address="Ул. Луначарского, Ленинский район" address="Ул. Луначарского, Ленинский район"
/> />
</Link>
<Object <Object
image={objectPicTwo} image={objectPicTwo}
price="1 234 567 ₽" price="1 234 567 ₽"
@@ -80,6 +96,24 @@ const SliderComponent = () => {
</Slider> </Slider>
</div> </div>
); );
}
return (
<div className="slider-objects">
<Slider {...settings}>
{objects.map((object) => (
<Link key={object.id} className="objects-link" to={`/apartament/${object.id}`}>
<Object
image={object.photoUrl}
price={object.price}
desc={object.title}
address={object.address}
/>
</Link>
))}
</Slider>
</div>
);
}; };
export { SliderComponent }; export { SliderComponent };

70
src/config/contacts.js Normal file
View File

@@ -0,0 +1,70 @@
export const CONTACTS = {
phone: {
display: "8(351)217-00-74",
link: "tel:+73512170074",
},
email: {
display: "sdelka.74@yandex.ru",
link: "mailto:sdelka.74@yandex.ru",
},
address: {
main: {
street: "ул. Комаровского, 4А, офис 210",
city: "Челябинск",
postalCode: "454052",
full: "ул. Комаровского, 4А, офис 210, Челябинск, 454052",
},
office: {
street: "Ленина, д. 60 В, оф. 701",
city: "Челябинск",
description: "Вход в офис со двора",
full: "Ленина, д. 60 В, оф. 701, Челябинск",
},
},
coordinates: [55.242355, 61.37697],
social: {
vk: {
url: "https://vk.com/almavid_74",
name: "ВКонтакте",
},
telegram: {
url: "https://t.me/almavid_74",
name: "Telegram",
},
instagram: {
url: "https://instagram.com/almavid_74",
name: "Instagram",
},
},
companyName: "Агентство недвижимости АЛМА-ВИД",
companyNameShort: "АЛМА-ВИД",
workingHours: "Пн-Пт: 9:00-18:00, Сб: 10:00-16:00",
getBalloonContent: () => {
return `
<div>
<strong>${CONTACTS.companyName}</strong><br/>
Адрес: ${CONTACTS.address.main.street}<br/>
${CONTACTS.address.main.city}, ${CONTACTS.address.main.postalCode}<br/>
Тел: ${CONTACTS.phone.display}
</div>
`;
},
};
export const API_CONFIG = {
baseURL: "https://almavid.ngr1.ru",
endpoints: {
feedback: "/api/feedback/send",
rental: "/api/rental/send",
},
getFullURL: (endpoint) => {
return `${API_CONFIG.baseURL}${endpoint}`;
},
};

15
src/hooks/useContacts.js Normal file
View File

@@ -0,0 +1,15 @@
import { CONTACTS } from "../config/contacts";
// Хук для использования контактной информации
export const useContacts = () => {
return {
...CONTACTS,
// Дополнительные утилитарные функции
getPhoneLink: () => CONTACTS.phone.link,
getEmailLink: () => CONTACTS.email.link,
getFullAddress: (type = "main") =>
CONTACTS.address[type]?.full || CONTACTS.address.main.full,
getSocialLink: (platform) => CONTACTS.social[platform]?.url || "#",
getSocialName: (platform) => CONTACTS.social[platform]?.name || platform,
};
};

View File

@@ -1,12 +1,13 @@
import { useEffect } from 'react'; import { useEffect } from "react";
import './About.scss'; import "./About.scss";
import { Header } from '../../components/Header/Header'; import { Header } from "../../components/Header/Header";
import { Form } from '../../components/Form/Form'; import { Form } from "../../components/Form/Form";
import { SliderComponent } from '../../components/Sliders/Slider'; import { SliderComponent } from "../../components/Sliders/Slider";
import { CONTACTS } from "../../config/contacts";
function About() { function About() {
useEffect(() => { useEffect(() => {
document.title = 'Об Агентстве недвижимости АЛМА-ВИД'; document.title = `Об ${CONTACTS.companyName}`;
}, []); }, []);
return ( return (
@@ -16,18 +17,28 @@ function About() {
<section className="about"> <section className="about">
<div className="about-inner"> <div className="about-inner">
<div className="about__info font-inter-bold"> <div className="about__info font-inter-bold">
<p>Каждый из нас хоть раз сталкивается с квартирным вопросом - покупка, продажа, <p>
обмен квартиры или дома - эти процессы требуют серьезного профессионального подхода Каждый из нас хоть раз сталкивается с квартирным вопросом -
и юридической грамотности.</p> покупка, продажа, обмен квартиры или дома - эти процессы требуют
<p>Агентство недвижимости АЛМА-ВИД существует с 2000 года за этот период мы обрели серьезного профессионального подхода и юридической грамотности.
доверие и уважение наших многочисленных клиентов.</p> </p>
<p>Мы находимся в постоянном развитии и оттачиваем профессионализм наших сотрудников, <p>
обладая серьезной материальной базой и налаженными коммуникациями с крупными банками. Агентство недвижимости АЛМА-ВИД существует с 2000 года за этот
Благодаря этому мы имеем возможность предоставлять услуги период мы обрели доверие и уважение наших многочисленных
в сфере недвижимости высокого качества.</p> клиентов.
</p>
<p>
Мы находимся в постоянном развитии и оттачиваем профессионализм
наших сотрудников, обладая серьезной материальной базой и
налаженными коммуникациями с крупными банками. Благодаря этому
мы имеем возможность предоставлять услуги в сфере недвижимости
высокого качества.
</p>
</div> </div>
</div> </div>
<div className="about__title font-inter-semibold">Об Агентстве недвижимости <span>АЛМА-Вид</span></div> <div className="about__title font-inter-semibold">
Об {CONTACTS.companyName} <span>{CONTACTS.companyNameShort}</span>
</div>
</section> </section>
</div> </div>
<section className="certificates"> <section className="certificates">

View File

@@ -1,22 +1,43 @@
import { useEffect } from 'react'; import { useEffect, useState } from "react";
import './Apartament.scss'; import "./Apartament.scss";
import { YMaps, Map, } from '@pbe/react-yandex-maps'; import { YMaps, Map, Placemark } from "@pbe/react-yandex-maps";
import { Header } from '../../components/Header/Header'; import { Header } from "../../components/Header/Header";
import { Form } from '../../components/Form/Form'; import { Form } from "../../components/Form/Form";
import { Gallery } from '../../components/Gallery/Gallery'; import { Gallery } from "../../components/Gallery/Gallery";
import { CONTACTS } from "../../config/contacts";
function Apartament() { function Apartament() {
const [isMapActive, setIsMapActive] = useState(false);
useEffect(() => { useEffect(() => {
document.title = 'Апартаменты'; document.title = "Апартаменты";
}, []); }, []);
const handleActivateMap = () => {
setIsMapActive(true);
if (window.apartamentMap) {
window.apartamentMap.behaviors.enable("scrollZoom");
window.apartamentMap.behaviors.enable("drag");
}
};
const handleDeactivateMap = () => {
setIsMapActive(false);
if (window.apartamentMap) {
window.apartamentMap.behaviors.disable("scrollZoom");
window.apartamentMap.behaviors.disable("drag");
}
};
return ( return (
<> <>
<Header /> <Header />
<section className="apartament"> <section className="apartament">
<h2 className="apartament__title font-inter-bold">1-комн. кв. 34 м</h2> <h2 className="apartament__title font-inter-bold">1-комн. кв. 34 м</h2>
<p className="apartament__para font-inter-regular">Ул. Луначарского, Ленинский район</p> <p className="apartament__para font-inter-regular">
Ул. Луначарского, Ленинский район
</p>
<div className="apartament__container"> <div className="apartament__container">
<div className="apartament__info"> <div className="apartament__info">
<Gallery /> <Gallery />
@@ -24,28 +45,36 @@ function Apartament() {
<div className="icons__item"> <div className="icons__item">
<div className="icons__pic icons__pic_area"></div> <div className="icons__pic icons__pic_area"></div>
<div className="icons__info"> <div className="icons__info">
<p className="icons__desc font-inter-regular ">Общая площадь</p> <p className="icons__desc font-inter-regular ">
Общая площадь
</p>
<p className="icons__num font-inter-semibold">72М</p> <p className="icons__num font-inter-semibold">72М</p>
</div> </div>
</div> </div>
<div className="icons__item"> <div className="icons__item">
<div className="icons__pic icons__pic_living"></div> <div className="icons__pic icons__pic_living"></div>
<div className="icons__info"> <div className="icons__info">
<p className="icons__desc font-inter-regular ">Жилая площадь</p> <p className="icons__desc font-inter-regular ">
Жилая площадь
</p>
<p className="icons__num font-inter-semibold">68М</p> <p className="icons__num font-inter-semibold">68М</p>
</div> </div>
</div> </div>
<div className="icons__item"> <div className="icons__item">
<div className="icons__pic icons__pic_kitchen"></div> <div className="icons__pic icons__pic_kitchen"></div>
<div className="icons__info"> <div className="icons__info">
<p className="icons__desc font-inter-regular ">Площадь кухни</p> <p className="icons__desc font-inter-regular ">
Площадь кухни
</p>
<p className="icons__num font-inter-semibold">11М</p> <p className="icons__num font-inter-semibold">11М</p>
</div> </div>
</div> </div>
<div className="icons__item"> <div className="icons__item">
<div className="icons__pic icons__pic_year"></div> <div className="icons__pic icons__pic_year"></div>
<div className="icons__info"> <div className="icons__info">
<p className="icons__desc font-inter-regular ">Год постройки</p> <p className="icons__desc font-inter-regular ">
Год постройки
</p>
<p className="icons__num font-inter-semibold">2003</p> <p className="icons__num font-inter-semibold">2003</p>
</div> </div>
</div> </div>
@@ -58,42 +87,82 @@ function Apartament() {
</div> </div>
</div> </div>
<div className="apartament__text font-inter-regular"> <div className="apartament__text font-inter-regular">
<p>Описани хаты рандомный текст Lorem Ipsum is simply dummy text of <p>
Описани хаты рандомный текст Lorem Ipsum is simply dummy text of
the printing and typesetting industry. Lorem Ipsum has been the the printing and typesetting industry. Lorem Ipsum has been the
industry's standard dummy text ever since the 1500s, when an unknown industry's standard dummy text ever since the 1500s, when an
printer took a galley of type and scrambled it to make a type specimen book. unknown printer took a galley of type and scrambled it to make a
It has survived not only five centuries, but also the leap into electronic type specimen book. It has survived not only five centuries, but
typesetting, remaining essentially unchanged. It was popularised in the 1960s also the leap into electronic typesetting, remaining essentially
with the release of Letraset sheets containing Lorem Ipsum passages, and more unchanged. It was popularised in the 1960s with the release of
recently with desktop publishing software like Aldus PageMaker including Letraset sheets containing Lorem Ipsum passages, and more
versions of Lorem Ipsum.</p> recently with desktop publishing software like Aldus PageMaker
including versions of Lorem Ipsum.
</p>
</div> </div>
<div className="apartament__desc font-inter-regular"> <div className="apartament__desc font-inter-regular">
<div className="apartament__flat flat"> <div className="apartament__flat flat">
<p className="flat__title font-inter-bold">О квартире</p> <p className="flat__title font-inter-bold">О квартире</p>
<ul> <ul>
<li><span className="title-color">Тип жилья:</span> вторичный</li> <li>
<li><span className="title-color">Общая площадь:</span> 31.9 м²</li> <span className="title-color">Тип жилья:</span> вторичный
<li><span className="title-color">Площадь кухни:</span> 4 м²</li> </li>
<li><span className="title-color">Жилая площадь:</span> 21 м²</li> <li>
<li><span className="title-color">Этаж:</span> 4 из 5</li> <span className="title-color">Общая площадь:</span> 31.9 м²
<li><span className="title-color">Балкон или лоджия:</span> лоджия</li> </li>
<li><span className="title-color">Высота потолков:</span> 2.5 м</li> <li>
<li><span className="title-color">Санузел:</span> совмещенный</li> <span className="title-color">Площадь кухни:</span> 4 м²
<li><span className="title-color">Вид из окон:</span> во двор</li> </li>
<li><span className="title-color">Ремонт:</span> косметический</li> <li>
<span className="title-color">Жилая площадь:</span> 21 м²
</li>
<li>
<span className="title-color">Этаж:</span> 4 из 5
</li>
<li>
<span className="title-color">Балкон или лоджия:</span>{" "}
лоджия
</li>
<li>
<span className="title-color">Высота потолков:</span> 2.5 м
</li>
<li>
<span className="title-color">Санузел:</span> совмещенный
</li>
<li>
<span className="title-color">Вид из окон:</span> во двор
</li>
<li>
<span className="title-color">Ремонт:</span> косметический
</li>
</ul> </ul>
</div> </div>
<div className="apartament__home home"> <div className="apartament__home home">
<p className="home__title font-inter-bold">О доме</p> <p className="home__title font-inter-bold">О доме</p>
<ul> <ul>
<li><span className="title-color">Год постройки:</span> 2003</li> <li>
<li><span className="title-color">Тип дома:</span> панельный</li> <span className="title-color">Год постройки:</span> 2003
<li><span className="title-color">Тип перекрытий:</span> железобетонный</li> </li>
<li><span className="title-color">Подъезды:</span> 5</li> <li>
<li><span className="title-color">Отопление:</span> центральное</li> <span className="title-color">Тип дома:</span> панельный
<li><span className="title-color">Аварийность:</span> нет</li> </li>
<li><span className="title-color">Газоснобжение:</span> центральное</li> <li>
<span className="title-color">Тип перекрытий:</span>{" "}
железобетонный
</li>
<li>
<span className="title-color">Подъезды:</span> 5
</li>
<li>
<span className="title-color">Отопление:</span> центральное
</li>
<li>
<span className="title-color">Аварийность:</span> нет
</li>
<li>
<span className="title-color">Газоснобжение:</span>{" "}
центральное
</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -104,16 +173,150 @@ function Apartament() {
<div className="price__logo"></div> <div className="price__logo"></div>
</div> </div>
<p className="price__desc">В ипотеку от 6546823 /мес</p> <p className="price__desc">В ипотеку от 6546823 /мес</p>
<p className="price__meter">Цена за метр............................................................45656 /м</p> <p className="price__meter">
<p className="price__conditions">Условия сделки......................................свободная продажа</p> Цена за
<p className="price__mortgage">Ипотека......................................................................возможна</p> метр............................................................45656
<button className="apartament__btn_number font-inter-bold" type="button">Показать телефон</button> /м
<button className="apartament__btn_write font-inter-bold" type="button">Написать</button> </p>
<p className="price__conditions">
Условия сделки......................................свободная
продажа
</p>
<p className="price__mortgage">
Ипотека......................................................................возможна
</p>
<button
className="apartament__btn_number font-inter-bold"
type="button"
>
Показать телефон
</button>
<button
className="apartament__btn_write font-inter-bold"
type="button"
>
Написать
</button>
</div> </div>
</div> </div>
<div style={{ position: "relative" }}>
<YMaps> <YMaps>
<Map className="map-apartament" defaultState={{ center: [55.16, 61.4], zoom: 15 }} /> <Map
className="map-apartament"
defaultState={{
center: CONTACTS.coordinates,
zoom: 15,
}}
options={{
scrollZoom: false,
drag: false,
suppressMapOpenBlock: true,
}}
instanceRef={(ref) => {
window.apartamentMap = ref;
}}
>
<Placemark
geometry={CONTACTS.coordinates}
properties={{
balloonContent: CONTACTS.getBalloonContent(),
hintContent: `Офис ${CONTACTS.companyNameShort}`,
}}
options={{
preset: "islands#redDotIcon",
}}
/>
</Map>
{/* Блокирующий оверлей, когда карта неактивна */}
{!isMapActive && (
<div
onClick={handleActivateMap}
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(255, 255, 255, 0.1)",
zIndex: 999,
cursor: "pointer",
}}
/>
)}
{/* Кнопка управления картой в центре */}
<div
style={{
position: "absolute",
top: "85%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 1000,
}}
>
{!isMapActive ? (
<button
onClick={handleActivateMap}
style={{
background: "rgba(0, 123, 255, 0.9)",
border: "none",
borderRadius: "8px",
padding: "15px 25px",
fontSize: "16px",
fontFamily: "Inter, Arial, sans-serif",
cursor: "pointer",
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
color: "white",
fontWeight: "bold",
minWidth: "140px",
textAlign: "center",
transition: "all 0.3s ease",
}}
onMouseEnter={(e) => {
e.target.style.background = "rgba(0, 123, 255, 1)";
e.target.style.transform = "scale(1.05)";
}}
onMouseLeave={(e) => {
e.target.style.background = "rgba(0, 123, 255, 0.9)";
e.target.style.transform = "scale(1)";
}}
>
📍 Посмотреть карту
</button>
) : (
<button
onClick={handleDeactivateMap}
style={{
background: "rgba(220, 53, 69, 0.9)",
border: "none",
borderRadius: "6px",
padding: "10px 20px",
fontSize: "14px",
fontFamily: "Inter, Arial, sans-serif",
cursor: "pointer",
boxShadow: "0 3px 8px rgba(0,0,0,0.3)",
color: "white",
fontWeight: "bold",
minWidth: "120px",
textAlign: "center",
transition: "all 0.3s ease",
}}
onMouseEnter={(e) => {
e.target.style.background = "rgba(220, 53, 69, 1)";
e.target.style.transform = "scale(1.05)";
}}
onMouseLeave={(e) => {
e.target.style.background = "rgba(220, 53, 69, 0.9)";
e.target.style.transform = "scale(1)";
}}
>
Закрыть карту
</button>
)}
</div>
</YMaps> </YMaps>
</div>
</section> </section>
<Form scrolledThreshold={2350} /> <Form scrolledThreshold={2350} />
</> </>

View File

@@ -1,16 +1,35 @@
import { useEffect } from "react"; import { useEffect, useState } from "react";
import "./Home.scss"; import "./Home.scss";
import { YMaps, Map } from "@pbe/react-yandex-maps"; import { YMaps, Map, Placemark } from "@pbe/react-yandex-maps";
import { Header } from "../../components/Header/Header"; import { Header } from "../../components/Header/Header";
import { Form } from "../../components/Form/Form"; import { Form } from "../../components/Form/Form";
import lawyer from "../../assets/images/lawyer/Mask-group.svg"; import lawyer from "../../assets/images/lawyer/Mask-group.svg";
import { CONTACTS } from "../../config/contacts";
function Home() { function Home() {
const [isMapActive, setIsMapActive] = useState(false);
useEffect(() => { useEffect(() => {
document.title = "Агентство недвижимости АЛМА-ВИД"; document.title = CONTACTS.companyName;
}, []); }, []);
const handleActivateMap = () => {
setIsMapActive(true);
if (window.homeMap) {
window.homeMap.behaviors.enable("scrollZoom");
window.homeMap.behaviors.enable("drag");
}
};
const handleDeactivateMap = () => {
setIsMapActive(false);
if (window.homeMap) {
window.homeMap.behaviors.disable("scrollZoom");
window.homeMap.behaviors.disable("drag");
}
};
return ( return (
<> <>
<div className="wrapper"> <div className="wrapper">
@@ -90,8 +109,118 @@ function Home() {
<YMaps> <YMaps>
<Map <Map
className="map" className="map"
defaultState={{ center: [55.16, 61.4], zoom: 12 }} defaultState={{
center: CONTACTS.coordinates,
zoom: 12,
}}
options={{
scrollZoom: false,
drag: false,
suppressMapOpenBlock: true,
}}
instanceRef={(ref) => {
window.homeMap = ref;
}}
>
<Placemark
geometry={CONTACTS.coordinates}
properties={{
balloonContent: CONTACTS.getBalloonContent(),
hintContent: `Офис ${CONTACTS.companyNameShort}`,
}}
options={{
preset: "islands#redDotIcon",
}}
/> />
</Map>
{/* Блокирующий оверлей когда карта неактивна */}
{!isMapActive && (
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(255, 255, 255, 0.1)",
zIndex: 999,
cursor: "pointer",
}}
onClick={handleActivateMap}
/>
)}
{/* Кнопка управления картой в центре */}
<div
style={{
position: "absolute",
top: "85%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 1000,
}}
>
{!isMapActive ? (
<button
onClick={handleActivateMap}
style={{
background: "rgba(0, 123, 255, 0.9)",
border: "none",
borderRadius: "8px",
padding: "15px 25px",
fontSize: "16px",
fontFamily: "Inter, Arial, sans-serif",
cursor: "pointer",
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
color: "white",
fontWeight: "bold",
minWidth: "140px",
textAlign: "center",
transition: "all 0.3s ease",
}}
onMouseEnter={(e) => {
e.target.style.background = "rgba(0, 123, 255, 1)";
e.target.style.transform = "scale(1.05)";
}}
onMouseLeave={(e) => {
e.target.style.background = "rgba(0, 123, 255, 0.9)";
e.target.style.transform = "scale(1)";
}}
>
📍 Посмотреть карту
</button>
) : (
<button
onClick={handleDeactivateMap}
style={{
background: "rgba(220, 53, 69, 0.9)",
border: "none",
borderRadius: "6px",
padding: "10px 20px",
fontSize: "14px",
fontFamily: "Inter, Arial, sans-serif",
cursor: "pointer",
boxShadow: "0 3px 8px rgba(0,0,0,0.3)",
color: "white",
fontWeight: "bold",
minWidth: "120px",
textAlign: "center",
transition: "all 0.3s ease",
}}
onMouseEnter={(e) => {
e.target.style.background = "rgba(220, 53, 69, 1)";
e.target.style.transform = "scale(1.05)";
}}
onMouseLeave={(e) => {
e.target.style.background = "rgba(220, 53, 69, 0.9)";
e.target.style.transform = "scale(1)";
}}
>
Закрыть карту
</button>
)}
</div>
</YMaps> </YMaps>
</div> </div>
<Form scrolledThreshold={2750} /> <Form scrolledThreshold={2750} />

View File

@@ -120,6 +120,7 @@
.map-container { .map-container {
padding: 0 146px 80px 146px; padding: 0 146px 80px 146px;
position: relative;
@media (max-width: $laptopWidth) { @media (max-width: $laptopWidth) {
padding: 0px 50px 80px 50px; padding: 0px 50px 80px 50px;
@@ -143,7 +144,6 @@
} }
} }
.hero-tagline { .hero-tagline {
height: 100%; height: 100%;
display: flex; display: flex;
@@ -163,9 +163,7 @@
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7); text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7);
} }
@media (max-width: $tabletWidth) { @media (max-width: $tabletWidth) {
padding: 15px 30px 0; padding: 15px 30px 0;
p { p {
font-size: 18px; font-size: 18px;
@@ -174,16 +172,13 @@
} }
@media (max-width: $mobileWidth) { @media (max-width: $mobileWidth) {
order: 1; order: 1;
width: 100%; width: 100%;
margin-top: 20px; margin-top: 20px;
justify-content: center; justify-content: center;
padding: 10px 15px 0; padding: 10px 15px 0;
p { p {
font-size: 16px; font-size: 16px;
text-align: center; text-align: center;

View File

@@ -1,13 +1,14 @@
import { useEffect } from 'react'; import { useEffect } from "react";
import './Objects.scss'; import "./Objects.scss";
import { Header } from '../../components/Header/Header'; import { Header } from "../../components/Header/Header";
import { Form } from '../../components/Form/Form'; import { Form } from "../../components/Form/Form";
import { SliderComponent } from '../../components/Sliders/SliderObjects'; import { SliderComponent } from "../../components/Sliders/SliderObjects";
import { CONTACTS } from "../../config/contacts";
function Objects() { function Objects() {
useEffect(() => { useEffect(() => {
document.title = 'Объекты Агентства недвижимости АЛМА-ВИД'; document.title = `Объекты ${CONTACTS.companyName}`;
}, []); }, []);
return ( return (

View File

@@ -1,35 +1,60 @@
import { useEffect } from 'react'; import { useEffect, useState } from "react";
import './Office.scss'; import "./Office.scss";
import { YMaps, Map } from '@pbe/react-yandex-maps'; import { YMaps, Map, Placemark } from "@pbe/react-yandex-maps";
import { Header } from '../../components/Header/Header'; import { Header } from "../../components/Header/Header";
import { CONTACTS } from "../../config/contacts";
function Office() { function Office() {
const [isMapActive, setIsMapActive] = useState(false);
useEffect(() => { useEffect(() => {
document.title = 'Контакты Агентства недвижимости АЛМА-ВИД'; document.title = `Контакты ${CONTACTS.companyName}`;
}, []); }, []);
const handleActivateMap = () => {
setIsMapActive(true);
if (window.officeMap) {
window.officeMap.behaviors.enable("scrollZoom");
window.officeMap.behaviors.enable("drag");
}
};
const handleDeactivateMap = () => {
setIsMapActive(false);
if (window.officeMap) {
window.officeMap.behaviors.disable("scrollZoom");
window.officeMap.behaviors.disable("drag");
}
};
return ( return (
<> <>
<div className="wrapper-office"> <div className="wrapper-office">
<Header /> <Header />
<section className="office"> <section className="office">
<div className="office__title font-inter-semibold">АЛМАВИД<span>агентство недвижимости</span></div> <div className="office__title font-inter-semibold">
{CONTACTS.companyNameShort}ВИД<span>агентство недвижимости</span>
</div>
<div className="office__separator"></div> <div className="office__separator"></div>
<div className="office-inner"> <div className="office-inner">
<div className="office-container"> <div className="office-container">
<div className="office__item"> <div className="office__item">
<div className="office__pic office__pic_purchase"></div> <div className="office__pic office__pic_purchase"></div>
<div className="office__text"> <div className="office__text">
<p className="office__desc font-inter-bold">Покупка, продажа, <p className="office__desc font-inter-bold">
<span>обмен квартир и комнат</span></p> Покупка, продажа,
<span>обмен квартир и комнат</span>
</p>
</div> </div>
</div> </div>
<div className="office__item"> <div className="office__item">
<div className="office__pic office__pic_rent"></div> <div className="office__pic office__pic_rent"></div>
<div className="office__text"> <div className="office__text">
<p className="office__desc font-inter-bold">Аренда квартир <p className="office__desc font-inter-bold">
<span>и комнат</span></p> Аренда квартир
<span>и комнат</span>
</p>
</div> </div>
</div> </div>
<div className="office__item"> <div className="office__item">
@@ -41,15 +66,19 @@ function Office() {
<div className="office__item"> <div className="office__item">
<div className="office__pic office__pic_country"></div> <div className="office__pic office__pic_country"></div>
<div className="office__text"> <div className="office__text">
<p className="office__desc font-inter-bold">Загородная <p className="office__desc font-inter-bold">
<span>недвижимость</span></p> Загородная
<span>недвижимость</span>
</p>
</div> </div>
</div> </div>
<div className="office__item"> <div className="office__item">
<div className="office__pic office__pic_commercial"></div> <div className="office__pic office__pic_commercial"></div>
<div className="office__text"> <div className="office__text">
<p className="office__desc font-inter-bold">Коммерческая <p className="office__desc font-inter-bold">
<span>недвижимость</span></p> Коммерческая
<span>недвижимость</span>
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -59,15 +88,145 @@ function Office() {
<section className="office-location font-inter-bold"> <section className="office-location font-inter-bold">
<h2 className="office-location__title">Приходите в наш офис</h2> <h2 className="office-location__title">Приходите в наш офис</h2>
<div className="office-info"> <div className="office-info">
<p className="office-info__para_address"><span>Адрес:</span>Ленина, д. 60 В, оф. 701 <p className="office-info__para_address">
<span className="office-info__desc">Вход в офис со двора</span></p> <span>Адрес:</span>
<p className="office-info__para"><span>Телефон:</span>+7 (351) 217-00-74 Заказать звонок</p> {CONTACTS.address.office.full}
<p className="office-info__para"><span>E-mail:</span>sdelka.74@yandex.ru</p> <span className="office-info__desc">
<p className="office-info__para"><span>Соц.сети:</span><a href="#">ВКонтакте</a></p> {CONTACTS.address.office.description}
</span>
</p>
<p className="office-info__para">
<span>Телефон:</span>
<a href={CONTACTS.phone.link}>{CONTACTS.phone.display}</a>
Заказать звонок
</p>
<p className="office-info__para">
<span>E-mail:</span>
<a href={CONTACTS.email.link}>{CONTACTS.email.display}</a>
</p>
<p className="office-info__para">
<span>Соц.сети:</span>
<a href={CONTACTS.social.vk.url}>{CONTACTS.social.vk.name}</a>
</p>
</div> </div>
<div style={{ position: "relative" }}>
<YMaps> <YMaps>
<Map className="map-office" defaultState={{ center: [55.16, 61.4], zoom: 12 }} /> <Map
className="map-office"
defaultState={{
center: CONTACTS.coordinates,
zoom: 16,
}}
options={{
scrollZoom: false,
drag: false,
suppressMapOpenBlock: true,
}}
instanceRef={(ref) => {
window.officeMap = ref;
}}
>
<Placemark
geometry={CONTACTS.coordinates}
properties={{
balloonContent: CONTACTS.getBalloonContent(),
hintContent: `Офис ${CONTACTS.companyNameShort}`,
}}
options={{
preset: "islands#redDotIcon",
}}
/>
</Map>
{/* Блокирующий оверлей, когда карта неактивна */}
{!isMapActive && (
<div
onClick={handleActivateMap}
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(255, 255, 255, 0.1)",
zIndex: 999,
cursor: "pointer",
}}
/>
)}
{/* Кнопка управления картой в центре */}
<div
style={{
position: "absolute",
top: "85%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 1000,
}}
>
{!isMapActive ? (
<button
onClick={handleActivateMap}
style={{
background: "rgba(0, 123, 255, 0.9)",
border: "none",
borderRadius: "8px",
padding: "15px 25px",
fontSize: "16px",
fontFamily: "Inter, Arial, sans-serif",
cursor: "pointer",
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
color: "white",
fontWeight: "bold",
minWidth: "140px",
textAlign: "center",
transition: "all 0.3s ease",
}}
onMouseEnter={(e) => {
e.target.style.background = "rgba(0, 123, 255, 1)";
e.target.style.transform = "scale(1.05)";
}}
onMouseLeave={(e) => {
e.target.style.background = "rgba(0, 123, 255, 0.9)";
e.target.style.transform = "scale(1)";
}}
>
📍 Посмотреть карту
</button>
) : (
<button
onClick={handleDeactivateMap}
style={{
background: "rgba(220, 53, 69, 0.9)",
border: "none",
borderRadius: "6px",
padding: "10px 20px",
fontSize: "14px",
fontFamily: "Inter, Arial, sans-serif",
cursor: "pointer",
boxShadow: "0 3px 8px rgba(0,0,0,0.3)",
color: "white",
fontWeight: "bold",
minWidth: "120px",
textAlign: "center",
transition: "all 0.3s ease",
}}
onMouseEnter={(e) => {
e.target.style.background = "rgba(220, 53, 69, 1)";
e.target.style.transform = "scale(1.05)";
}}
onMouseLeave={(e) => {
e.target.style.background = "rgba(220, 53, 69, 0.9)";
e.target.style.transform = "scale(1)";
}}
>
Закрыть карту
</button>
)}
</div>
</YMaps> </YMaps>
</div>
</section> </section>
</> </>
); );

View File

@@ -1,12 +1,13 @@
import { useEffect } from 'react'; import { useEffect } from "react";
import './Services.scss'; import "./Services.scss";
import { Header } from '../../components/Header/Header'; import { Header } from "../../components/Header/Header";
import { Form } from '../../components/Form/Form'; import { Form } from "../../components/Form/Form";
import { CONTACTS } from "../../config/contacts";
function Services() { function Services() {
useEffect(() => { useEffect(() => {
document.title = 'Услуги Агентства недвижимости АЛМА-ВИД'; document.title = `Услуги ${CONTACTS.companyName}`;
}, []); }, []);
return ( return (
@@ -14,71 +15,101 @@ function Services() {
<div className="wrapper-services"> <div className="wrapper-services">
<Header /> <Header />
<div className="services__text"> <div className="services__text">
<p className="services__title font-inter-bold">Мы работаем с 2000 года и помогаем <p className="services__title font-inter-bold">
продать и купить жилую и коммерческую недвижимость.</p> Мы работаем с 2000 года и помогаем продать и купить жилую и
<p className="services__subtitle font-inter-bold">Получите бесплатную консультацию коммерческую недвижимость.
по покупке или продаже вашей </p>
недвижимости: <a className="services__link" href="#">Вконтакте</a></p> <p className="services__subtitle font-inter-bold">
Получите бесплатную консультацию по покупке или продаже вашей
недвижимости:{" "}
<a className="services__link" href={CONTACTS.social.vk.url}>
{CONTACTS.social.vk.name}
</a>
</p>
</div> </div>
</div> </div>
<section className="services-info font-inter-bold"> <section className="services-info font-inter-bold">
<h2 className="services-info__title">Почему более 3 000 семей доверили нам свою недвижимость:</h2> <h2 className="services-info__title">
Почему более 3 000 семей доверили нам свою недвижимость:
</h2>
<div className="services-info__container"> <div className="services-info__container">
<div className="services-info__desc"> <div className="services-info__desc">
<div className="services-info__item"> <div className="services-info__item">
<p className="services-info__subtitle">1. Высокое качество работы</p> <p className="services-info__subtitle">
<p className="services-info__text font-inter-regular">Наша деятельность проверена и признана 1. Высокое качество работы
соответствующей национальному </p>
стандарту добровольной сертификации услуг на рынке недвижимости <p className="services-info__text font-inter-regular">
Российской Федерации.</p> Наша деятельность проверена и признана соответствующей
национальному стандарту добровольной сертификации услуг на рынке
недвижимости Российской Федерации.
</p>
</div> </div>
<div className="services-info__item"> <div className="services-info__item">
<p className="services-info__subtitle">2. Работаем в соответствии <p className="services-info__subtitle">
с Законодательством РФ</p> 2. Работаем в соответствии с Законодательством РФ
<p className="services-info__text font-inter-regular">Юридическая проверка всех сделок </p>
и соблюдение закона о защите <p className="services-info__text font-inter-regular">
персональных данных делают сделки безопасными для наших клиентов.</p> Юридическая проверка всех сделок и соблюдение закона о защите
персональных данных делают сделки безопасными для наших
клиентов.
</p>
</div> </div>
<div className="services-info__item"> <div className="services-info__item">
<p className="services-info__subtitle">3. Эффективность</p> <p className="services-info__subtitle">3. Эффективность</p>
<p className="services-info__text font-inter-regular">Широкая база объектов недвижимости, <p className="services-info__text font-inter-regular">
профессиональная продажа и подбор, собственные рекламные алгоритмы, и огромный опыт Широкая база объектов недвижимости, профессиональная продажа и
в проведении сделок любой сложности позволяют нам гарантировать, подбор, собственные рекламные алгоритмы, и огромный опыт в
что вы получите максимальную выгоду от работы с нами.</p> проведении сделок любой сложности позволяют нам гарантировать,
что вы получите максимальную выгоду от работы с нами.
</p>
</div> </div>
<div className="services-info__item"> <div className="services-info__item">
<p className="services-info__subtitle">4. Выгодная процентная ставка по ипотеке</p> <p className="services-info__subtitle">
<p className="services-info__text font-inter-regular">Мы постоянно развиваем 4. Выгодная процентная ставка по ипотеке
партнерские отношения с банками, за счет чего </p>
имеем хорошую скидку для клиентов <p className="services-info__text font-inter-regular">
и высокую степень одобрения заявок.</p> Мы постоянно развиваем партнерские отношения с банками, за счет
чего имеем хорошую скидку для клиентов и высокую степень
одобрения заявок.
</p>
</div> </div>
</div> </div>
<div className="services-info__desc"> <div className="services-info__desc">
<div className="services-info__item"> <div className="services-info__item">
<p className="services-info__subtitle">5. Квалифицированные сотрудники</p> <p className="services-info__subtitle">
<p className="services-info__text font-inter-regular">Специалисты по недвижимости 5. Квалифицированные сотрудники
нашей компании проходят обучение и регулярную аттестацию, а также постоянно повышают свой </p>
профессиональный уровень. Это обязательное условие для каждого из нас. <p className="services-info__text font-inter-regular">
А значит, вы можете решить любой жилищный вопрос, воспользовавшись помощью Специалисты по недвижимости нашей компании проходят обучение и
профессиональных риэлторов, юристов и специалистов по ипотечному кредитованию.</p> регулярную аттестацию, а также постоянно повышают свой
профессиональный уровень. Это обязательное условие для каждого
из нас. А значит, вы можете решить любой жилищный вопрос,
воспользовавшись помощью профессиональных риэлторов, юристов и
специалистов по ипотечному кредитованию.
</p>
</div> </div>
<div className="services-info__item"> <div className="services-info__item">
<p className="services-info__subtitle">6. Индивидуальный подход</p> <p className="services-info__subtitle">
<p className="services-info__text font-inter-regular">Мы слышим и понимаем вас, делаем все, 6. Индивидуальный подход
чтобы в результате совместной работы задача, поставленная клиентом, </p>
была решена, и он рекомендовал нас в дальнейшем.</p> <p className="services-info__text font-inter-regular">
Мы слышим и понимаем вас, делаем все, чтобы в результате
совместной работы задача, поставленная клиентом, была решена, и
он рекомендовал нас в дальнейшем.
</p>
</div> </div>
<div className="services-info__item"> <div className="services-info__item">
<p className="services-info__subtitle">7. Гарантии</p> <p className="services-info__subtitle">7. Гарантии</p>
<p className="services-info__text font-inter-regular">Мы даем своим клиентам гарантии <p className="services-info__text font-inter-regular">
в юридической проверке сделок. </p> Мы даем своим клиентам гарантии в юридической проверке сделок.{" "}
</p>
</div> </div>
<div className="services-info__item"> <div className="services-info__item">
<p className="services-info__text"> <p className="services-info__text">
<span className="services-info__accent">Хотите купить, <span className="services-info__accent">
продать недвижимость или задать вопрос юристу, оставьте заявку наши специалисты Хотите купить, продать недвижимость или задать вопрос юристу,
проконсультируют вас.</span> оставьте заявку наши специалисты проконсультируют вас.
</span>
</p> </p>
</div> </div>
</div> </div>
@@ -92,8 +123,12 @@ function Services() {
<li>продажа недвижимости</li> <li>продажа недвижимости</li>
<li>покупка недвижимости</li> <li>покупка недвижимости</li>
<li>ипотека</li> <li>ипотека</li>
<li>подбор объектов <span>недвижимости</span></li> <li>
<li>помощь в оформлении <span>наследства</span></li> подбор объектов <span>недвижимости</span>
</li>
<li>
помощь в оформлении <span>наследства</span>
</li>
<li>консультации по вопросам недвижимости</li> <li>консультации по вопросам недвижимости</li>
<li>оформление приватизации</li> <li>оформление приватизации</li>
</ul> </ul>
@@ -103,10 +138,17 @@ function Services() {
<ul className="services__list_right"> <ul className="services__list_right">
<li>юридическое сопровождение сделок</li> <li>юридическое сопровождение сделок</li>
<li>узаконивание перепланировок</li> <li>узаконивание перепланировок</li>
<li>оформление права собственности на дома и земельные участки</li> <li>
<li>работа с жилищными сертификатами и субсидиями, в том числе материнским капиталом</li> оформление права собственности на дома и земельные участки
<li>составление (подготовка) юридической экспертизы на продаваемый/покупаемый </li>
объект недвижимости</li> <li>
работа с жилищными сертификатами и субсидиями, в том числе
материнским капиталом
</li>
<li>
составление (подготовка) юридической экспертизы на
продаваемый/покупаемый объект недвижимости
</li>
</ul> </ul>
</div> </div>
</div> </div>