mirror of
https://github.com/danilt2000/Alma-vid.git
synced 2025-12-08 19:29:26 +02:00
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:
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user