Add expandable address to Object component

Introduced address truncation and expand/collapse functionality in Object.jsx and updated related styles in Object.scss. Improved responsive layout and fallback data in SliderObjects.jsx. Removed unused office address from contacts.js and updated Office page to use main address.
This commit is contained in:
Madara0330E
2025-07-19 22:28:45 +05:00
parent 4adbf791ea
commit 3f59295b84
5 changed files with 387 additions and 228 deletions

View File

@@ -1,16 +1,55 @@
import './Object.scss';
import "./Object.scss";
import { useState } from "react";
function Object(props) {
return (
<div className="item">
<img className="item__image" src={props.image} alt="квартира" />
<div className="item__info font-inter-bold">
<p className="item__price">{props.price}</p>
<p className="item__desc">{props.desc}</p>
<p className="item__address font-inter-regular">{props.address}</p>
</div>
const [isExpanded, setIsExpanded] = useState(false);
const addressLimit = 30;
const isAddressTooLong = props.address && props.address.length > addressLimit;
const needsExpansion = isAddressTooLong;
const truncateAddress = (text) => {
if (!text) return "";
if (isExpanded) return text;
if (text.length <= addressLimit) return text;
const truncated = text.substring(0, addressLimit);
const lastSpaceIndex = truncated.lastIndexOf(" ");
if (lastSpaceIndex > 0) {
return truncated.substring(0, lastSpaceIndex) + "...";
}
return truncated + "...";
};
const toggleExpansion = () => {
setIsExpanded(!isExpanded);
};
return (
<div className={`item ${isExpanded ? "item--expanded" : ""}`}>
<img className="item__image" src={props.image} alt="квартира" />
<div className="item__info font-inter-bold">
<div className="item__content">
<p className="item__price">{props.price}</p>
<p className="item__desc" title={props.desc}>
{props.desc}
</p>
<p className="item__address font-inter-regular" title={props.address}>
{truncateAddress(props.address)}
</p>
</div>
);
{needsExpansion && (
<button
className="item__expand-btn"
onClick={toggleExpansion}
type="button"
>
{isExpanded ? "Скрыть" : "Показать больше"}
</button>
)}
</div>
</div>
);
}
export { Object };

View File

@@ -1,158 +1,281 @@
@import '../../styles/vars.scss';
@import "../../styles/vars.scss";
.item__info p {
margin: 0 0 12px 0;
margin: 0 0 12px 0;
}
.item {
display: flex;
flex-direction: column;
// width: 381px;
// height: 381px;
margin: 0 20px;
border-radius: 15px;
background-color: white;
display: flex;
flex-direction: column;
margin: 0 20px;
border-radius: 15px;
background-color: white;
min-height: 450px;
transition: height 0.3s ease;
@media (max-width: 1300px) {
height: 381px;
}
&--expanded {
height: auto;
min-height: 450px;
}
@media (max-width: 1200px) {
height: 361px;
}
@media (min-width: 1800px) {
min-height: 480px;
@media (max-width: 1100px) {
height: 341px;
&--expanded {
min-height: 480px;
}
}
@media (max-width: $laptopWidth) {
height: 321px;
margin: 0 15px;
}
@media (max-width: 1300px) {
min-height: 430px;
@media (max-width: $tabletWidth) {
height: 505px;
&--expanded {
min-height: 430px;
}
}
@media (max-width: 740px) {
height: 473px;
}
@media (max-width: 1200px) {
min-height: 410px;
@media (max-width: 690px) {
height: 435px;
&--expanded {
min-height: 410px;
}
}
@media (max-width: 635px) {
height: 400px;
}
@media (max-width: 1100px) {
min-height: 390px;
@media (max-width: 570px) {
height: 361px;
&--expanded {
min-height: 390px;
}
}
@media (max-width: $mobileWidth) {
height: 381px;
}
@media (max-width: $laptopWidth) {
min-height: 370px;
margin: 0 15px;
@media (max-width: 420px) {
height: 361px;
&--expanded {
min-height: 370px;
}
}
@media (max-width: $tabletWidth) {
min-height: 500px;
&--expanded {
min-height: 500px;
}
}
@media (max-width: 740px) {
min-height: 480px;
&--expanded {
min-height: 480px;
}
}
@media (max-width: 690px) {
min-height: 460px;
&--expanded {
min-height: 460px;
}
}
@media (max-width: 635px) {
min-height: 440px;
&--expanded {
min-height: 440px;
}
}
@media (max-width: 570px) {
min-height: 420px;
&--expanded {
min-height: 420px;
}
}
@media (max-width: $mobileWidth) {
min-height: 400px;
margin: 0 10px;
&--expanded {
min-height: 400px;
}
}
@media (max-width: 420px) {
min-height: 380px;
&--expanded {
min-height: 380px;
}
}
}
.item__image:hover {
text-decoration: none;
text-decoration: none;
}
.item__image {
border-top-right-radius: 15px;
border-top-left-radius: 15px;
width: 100%;
border-top-right-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;
object-fit: cover;
object-position: center;
display: block;
}
@media (max-width: 1300px) {
height: 230px;
}
@media (max-width: 635px) {
height: 220px;
}
@media (max-width: 1200px) {
height: 210px;
}
@media (max-width: 570px) {
height: 190px;
}
@media (max-width: 1100px) {
height: 190px;
}
@media (max-width: $mobileWidth) {
height: 210px;
}
@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;
}
@media (max-width: 420px) {
height: 190px;
}
}
.item__info {
margin: 25px 0 13px 25px;
margin: 20px 20px 15px 20px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 0;
@media (max-width: $mobileWidth) {
margin: 15px 15px 10px 15px;
}
}
.item__content {
flex: 1;
display: flex;
flex-direction: column;
}
.item__price {
font-size: 25px;
font-size: 25px;
@media (min-width: 1800px) {
font-size: 29px;
}
@media (min-width: 1800px) {
font-size: 29px;
}
@media (min-width: 768.98px) and (max-width: $laptopWidth) {
font-size: 23px;
}
@media (min-width: 768.98px) and (max-width: $laptopWidth) {
font-size: 23px;
}
}
.item__desc {
font-size: 20px;
font-size: 20px;
line-height: 1.4;
margin-bottom: 8px !important;
word-wrap: break-word;
hyphens: auto;
@media (min-width: 1800px) {
font-size: 24px;
}
@media (min-width: 1800px) {
font-size: 24px;
}
@media (min-width: 768.98px) and (max-width: $laptopWidth) {
font-size: 18px;
}
@media (min-width: 768.98px) and (max-width: $laptopWidth) {
font-size: 18px;
}
}
.item__address {
font-size: 17px;
font-size: 17px;
line-height: 1.3;
word-wrap: break-word;
hyphens: auto;
@media (min-width: 1800px) {
font-size: 21px;
}
@media (min-width: 1800px) {
font-size: 21px;
}
@media (min-width: 768.98px) and (max-width: $laptopWidth) {
font-size: 16px;
}
}
@media (min-width: 768.98px) and (max-width: $laptopWidth) {
font-size: 16px;
}
}
.item__expand-btn {
background: none;
border: none;
color: #007bff;
font-size: 11px;
font-weight: 500;
cursor: pointer;
padding: 4px 0;
margin-top: auto;
margin-bottom: 5px;
text-align: left;
transition: color 0.2s ease;
font-family: inherit;
flex-shrink: 0;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
color: #0056b3;
text-decoration: underline;
}
&:focus {
outline: none;
}
@media (min-width: 1800px) {
font-size: 13px;
padding: 5px 0;
}
@media (min-width: 768.98px) and (max-width: $laptopWidth) {
font-size: 10px;
padding: 3px 0;
}
@media (max-width: $mobileWidth) {
font-size: 11px;
margin-bottom: 3px;
}
}

View File

@@ -1,119 +1,124 @@
import React, { useState, useEffect } from 'react';
import Slider from 'react-slick';
import { Link } from 'react-router-dom';
import { useMediaQuery } from 'react-responsive';
import React, { useState, useEffect } from "react";
import Slider from "react-slick";
import { useMediaQuery } from "react-responsive";
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
import './SliderObjects.scss';
import { Object } from '../Object/Object';
import { API_CONFIG } from '../../config/contacts';
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import "./SliderObjects.scss";
import { Object } from "../Object/Object";
import { API_CONFIG } from "../../config/contacts";
import objectPicOne from '../../assets/images/apartaments/image-44.jpg';
import objectPicTwo from '../../assets/images/apartaments/image-45.jpg';
import objectPicThree from '../../assets/images/apartaments/image-46.jpg';
import objectPicOne from "../../assets/images/apartaments/image-44.jpg";
import objectPicTwo from "../../assets/images/apartaments/image-45.jpg";
import objectPicThree from "../../assets/images/apartaments/image-46.jpg";
const NextArrow = ({ onClick }) => {
return (
<div className="slider-objects__arrow slider-objects__arrow_next" onClick={onClick}></div>
);
return (
<div
className="slider-objects__arrow slider-objects__arrow_next"
onClick={onClick}
></div>
);
};
const PrevArrow = ({ onClick }) => {
return (
<div className="slider-objects__arrow slider-objects__arrow_prev" onClick={onClick}></div>
);
return (
<div
className="slider-objects__arrow slider-objects__arrow_prev"
onClick={onClick}
></div>
);
};
const SliderComponent = () => {
const [objects, setObjects] = useState([]);
const [loading, setLoading] = useState(true);
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': '*/*'
}
});
const [objects, setObjects] = useState([]);
const [loading, setLoading] = useState(true);
if (response.ok) {
const data = await response.json();
setObjects(data);
}
} catch (error) {
console.error('Ошибка при загрузке объектов:', error);
// В случае ошибки оставляем пустой массив, компонент не сломается
} finally {
setLoading(false);
}
};
const isMobileResolution = useMediaQuery({ maxWidth: 768 });
fetchObjects();
}, []);
const settings = {
dots: objects.length > (isMobileResolution ? 1 : 3), // Показываем точки если объектов больше чем влезает
infinite: objects.length > (isMobileResolution ? 1 : 3), // Бесконечная прокрутка только если объектов достаточно
speed: 500,
slidesToShow: isMobileResolution ? 1 : 3,
slidesToScroll: isMobileResolution ? 1 : 3,
arrows: true,
nextArrow: <NextArrow />,
prevArrow: <PrevArrow />,
// Загрузка данных с 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);
}
};
// Если загружаем данные, показываем статические объекты как fallback
if (loading || objects.length === 0) {
return (
<div className="slider-objects">
<Slider {...{...settings, dots: false, infinite: true}}>
<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="Ул. Солнечная, Заречный район"
/>
</Slider>
</div>
);
}
fetchObjects();
}, []);
const settings = {
dots: objects.length > (isMobileResolution ? 1 : 3),
infinite: objects.length > (isMobileResolution ? 1 : 3),
speed: 500,
slidesToShow: isMobileResolution ? 1 : 3,
slidesToScroll: isMobileResolution ? 1 : 3,
arrows: true,
nextArrow: <NextArrow />,
prevArrow: <PrevArrow />,
};
if (loading || objects.length === 0) {
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>
<div className="slider-objects">
<Slider {...{ ...settings, dots: false, infinite: true }}>
<Object
image={objectPicOne}
price="2 500 000 ₽"
desc="2-комн. кв., 47 м², 1/2 этаж"
address="Челябинская область, Челябинск, Лазурная улица, 14А"
/>
<Object
image={objectPicTwo}
price="4 200 000 ₽"
desc="3-комн. кв., 77 м², 4/4 этаж"
address="Челябинская область, Челябинск, улица Ярослава Гашека, 20"
/>
<Object
image={objectPicThree}
price="4 900 000 ₽"
desc="Квартира-студия, 27,6 м², 19/25 этаж"
address="Свердловская область, Екатеринбург, улица Студенческая, 80"
/>
</Slider>
</div>
);
}
return (
<div className="slider-objects">
<Slider {...settings}>
{objects.map((object) => (
<Object
key={object.id}
image={object.photoUrl}
price={object.price}
desc={object.title}
address={object.address}
/>
))}
</Slider>
</div>
);
};
export { SliderComponent };

View File

@@ -15,12 +15,6 @@ export const CONTACTS = {
postalCode: "454052",
full: "ул. Комаровского, 4А, офис 210, Челябинск, 454052",
},
office: {
street: "Ленина, д. 60 В, оф. 701",
city: "Челябинск",
description: "Вход в офис со двора",
full: "Ленина, д. 60 В, оф. 701, Челябинск",
},
},
coordinates: [55.242355, 61.37697],

View File

@@ -90,10 +90,8 @@ function Office() {
<div className="office-info">
<p className="office-info__para_address">
<span>Адрес:</span>
{CONTACTS.address.office.full}
<span className="office-info__desc">
{CONTACTS.address.office.description}
</span>
{CONTACTS.address.main.full}
<span className="office-info__desc">Вход в офис со двора</span>
</p>
<p className="office-info__para">
<span>Телефон:</span>