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) { function Object(props) {
return ( const [isExpanded, setIsExpanded] = useState(false);
<div className="item">
<img className="item__image" src={props.image} alt="квартира" /> const addressLimit = 30;
<div className="item__info font-inter-bold"> const isAddressTooLong = props.address && props.address.length > addressLimit;
<p className="item__price">{props.price}</p> const needsExpansion = isAddressTooLong;
<p className="item__desc">{props.desc}</p>
<p className="item__address font-inter-regular">{props.address}</p> const truncateAddress = (text) => {
</div> 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> </div>
); {needsExpansion && (
<button
className="item__expand-btn"
onClick={toggleExpansion}
type="button"
>
{isExpanded ? "Скрыть" : "Показать больше"}
</button>
)}
</div>
</div>
);
} }
export { Object }; export { Object };

View File

@@ -1,158 +1,281 @@
@import '../../styles/vars.scss'; @import "../../styles/vars.scss";
.item__info p { .item__info p {
margin: 0 0 12px 0; margin: 0 0 12px 0;
} }
.item { .item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
// width: 381px; margin: 0 20px;
// height: 381px; border-radius: 15px;
margin: 0 20px; background-color: white;
border-radius: 15px; min-height: 450px;
background-color: white; transition: height 0.3s ease;
@media (max-width: 1300px) { &--expanded {
height: 381px; height: auto;
} min-height: 450px;
}
@media (max-width: 1200px) { @media (min-width: 1800px) {
height: 361px; min-height: 480px;
}
@media (max-width: 1100px) { &--expanded {
height: 341px; min-height: 480px;
} }
}
@media (max-width: $laptopWidth) { @media (max-width: 1300px) {
height: 321px; min-height: 430px;
margin: 0 15px;
}
@media (max-width: $tabletWidth) { &--expanded {
height: 505px; min-height: 430px;
} }
}
@media (max-width: 740px) { @media (max-width: 1200px) {
height: 473px; min-height: 410px;
}
@media (max-width: 690px) { &--expanded {
height: 435px; min-height: 410px;
} }
}
@media (max-width: 635px) { @media (max-width: 1100px) {
height: 400px; min-height: 390px;
}
@media (max-width: 570px) { &--expanded {
height: 361px; min-height: 390px;
} }
}
@media (max-width: $mobileWidth) { @media (max-width: $laptopWidth) {
height: 381px; min-height: 370px;
} margin: 0 15px;
@media (max-width: 420px) { &--expanded {
height: 361px; 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 { .item__image:hover {
text-decoration: none; text-decoration: none;
} }
.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%; 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; height: 250px;
object-fit: cover; }
object-position: center;
display: block;
@media (max-width: 1300px) { @media (max-width: 635px) {
height: 230px; height: 220px;
} }
@media (max-width: 1200px) { @media (max-width: 570px) {
height: 210px; height: 190px;
} }
@media (max-width: 1100px) { @media (max-width: $mobileWidth) {
height: 190px; height: 210px;
} }
@media (max-width: $laptopWidth) { @media (max-width: 420px) {
height: 170px; height: 190px;
} }
@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 {
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 { .item__price {
font-size: 25px; font-size: 25px;
@media (min-width: 1800px) { @media (min-width: 1800px) {
font-size: 29px; font-size: 29px;
} }
@media (min-width: 768.98px) and (max-width: $laptopWidth) { @media (min-width: 768.98px) and (max-width: $laptopWidth) {
font-size: 23px; font-size: 23px;
} }
} }
.item__desc { .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) { @media (min-width: 1800px) {
font-size: 24px; font-size: 24px;
} }
@media (min-width: 768.98px) and (max-width: $laptopWidth) { @media (min-width: 768.98px) and (max-width: $laptopWidth) {
font-size: 18px; font-size: 18px;
} }
} }
.item__address { .item__address {
font-size: 17px; font-size: 17px;
line-height: 1.3;
word-wrap: break-word;
hyphens: auto;
@media (min-width: 1800px) { @media (min-width: 1800px) {
font-size: 21px; font-size: 21px;
} }
@media (min-width: 768.98px) and (max-width: $laptopWidth) { @media (min-width: 768.98px) and (max-width: $laptopWidth) {
font-size: 16px; 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 React, { useState, useEffect } from "react";
import Slider from 'react-slick'; import Slider from "react-slick";
import { Link } from 'react-router-dom'; import { useMediaQuery } from "react-responsive";
import { useMediaQuery } from 'react-responsive';
import 'slick-carousel/slick/slick.css'; 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 { 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";
import objectPicThree from '../../assets/images/apartaments/image-46.jpg'; import objectPicThree from "../../assets/images/apartaments/image-46.jpg";
const NextArrow = ({ onClick }) => { const NextArrow = ({ onClick }) => {
return ( return (
<div className="slider-objects__arrow slider-objects__arrow_next" onClick={onClick}></div> <div
); className="slider-objects__arrow slider-objects__arrow_next"
onClick={onClick}
></div>
);
}; };
const PrevArrow = ({ onClick }) => { const PrevArrow = ({ onClick }) => {
return ( return (
<div className="slider-objects__arrow slider-objects__arrow_prev" onClick={onClick}></div> <div
); className="slider-objects__arrow slider-objects__arrow_prev"
onClick={onClick}
></div>
);
}; };
const SliderComponent = () => { const SliderComponent = () => {
const [objects, setObjects] = useState([]); const [objects, setObjects] = useState([]);
const [loading, setLoading] = useState(true); 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': '*/*'
}
});
if (response.ok) { const isMobileResolution = useMediaQuery({ maxWidth: 768 });
const data = await response.json();
setObjects(data);
}
} catch (error) {
console.error('Ошибка при загрузке объектов:', error);
// В случае ошибки оставляем пустой массив, компонент не сломается
} finally {
setLoading(false);
}
};
fetchObjects(); // Загрузка данных с API
}, []); useEffect(() => {
const fetchObjects = async () => {
const settings = { try {
dots: objects.length > (isMobileResolution ? 1 : 3), // Показываем точки если объектов больше чем влезает const response = await fetch(
infinite: objects.length > (isMobileResolution ? 1 : 3), // Бесконечная прокрутка только если объектов достаточно API_CONFIG.getFullURL(API_CONFIG.endpoints.rental),
speed: 500, {
slidesToShow: isMobileResolution ? 1 : 3, method: "GET",
slidesToScroll: isMobileResolution ? 1 : 3, headers: {
arrows: true, accept: "*/*",
nextArrow: <NextArrow />, },
prevArrow: <PrevArrow />, }
);
if (response.ok) {
const data = await response.json();
setObjects(data);
}
} catch (error) {
console.error("Ошибка при загрузке объектов:", error);
} finally {
setLoading(false);
}
}; };
// Если загружаем данные, показываем статические объекты как fallback fetchObjects();
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>
);
}
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 ( return (
<div className="slider-objects"> <div className="slider-objects">
<Slider {...settings}> <Slider {...{ ...settings, dots: false, infinite: true }}>
{objects.map((object) => ( <Object
<Link key={object.id} className="objects-link" to={`/apartament/${object.id}`}> image={objectPicOne}
<Object price="2 500 000 ₽"
image={object.photoUrl} desc="2-комн. кв., 47 м², 1/2 этаж"
price={object.price} address="Челябинская область, Челябинск, Лазурная улица, 14А"
desc={object.title} />
address={object.address} <Object
/> image={objectPicTwo}
</Link> price="4 200 000 ₽"
))} desc="3-комн. кв., 77 м², 4/4 этаж"
</Slider> address="Челябинская область, Челябинск, улица Ярослава Гашека, 20"
</div> />
<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 }; export { SliderComponent };

View File

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

View File

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