Merge pull request #65 from SlimusMinus/add-back_from_map
added button back from map and fix post on map
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { API_URL, getImageUrl } from "../config/authConstants";
|
import { API_URL, getImageUrl } from "../config/authConstants";
|
||||||
import { useGeolocation, GEO_STATUS } from "../hooks/useGeolocation";
|
import { useGeolocation, GEO_STATUS } from "../hooks/useGeolocation";
|
||||||
|
|
||||||
@@ -40,7 +41,8 @@ function createMarkerLayout(color, imageUrl) {
|
|||||||
const FOCUS_ZOOM = 16;
|
const FOCUS_ZOOM = 16;
|
||||||
const COORD_MATCH_TOLERANCE = 0.005;
|
const COORD_MATCH_TOLERANCE = 0.005;
|
||||||
|
|
||||||
export default function MapView({ focusPostId, focusLat, focusLng, onPostClick }) {
|
export default function MapView({ focusPostId, focusLat, focusLng, onPostClick, onBack }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const mapRef = useRef(null);
|
const mapRef = useRef(null);
|
||||||
const ymapRef = useRef(null);
|
const ymapRef = useRef(null);
|
||||||
const userMarkerRef = useRef(null);
|
const userMarkerRef = useRef(null);
|
||||||
@@ -215,6 +217,15 @@ export default function MapView({ focusPostId, focusLat, focusLng, onPostClick }
|
|||||||
{geoStatus === GEO_STATUS.UNSUPPORTED && (
|
{geoStatus === GEO_STATUS.UNSUPPORTED && (
|
||||||
<div className="f-mapview-geo f-mapview-geo-warn">Геолокация недоступна</div>
|
<div className="f-mapview-geo f-mapview-geo-warn">Геолокация недоступна</div>
|
||||||
)}
|
)}
|
||||||
|
{onBack && (
|
||||||
|
<button type="button" className="f-mapview-back" onClick={onBack}>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" className="f-mapview-back-icon">
|
||||||
|
<path d="M19 12H5" />
|
||||||
|
<path d="m12 19-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
{t('map.backToListings')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -389,5 +389,8 @@
|
|||||||
"favorites": {
|
"favorites": {
|
||||||
"toggleError": "Failed to change the favorite status.",
|
"toggleError": "Failed to change the favorite status.",
|
||||||
"loadError": "Failed to load favorites"
|
"loadError": "Failed to load favorites"
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"backToListings": "Back to listings"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"favorites": "Избранное",
|
"favorites": "Избранное",
|
||||||
"items": "Вещи",
|
"items": "Вещи",
|
||||||
"pets": "Питомцы",
|
"pets": "Питомцы",
|
||||||
"chat": "Чат",
|
"chat": "Чаты по объявлениям",
|
||||||
"create": "Создать",
|
"create": "Создать",
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
"map": "Карта",
|
"map": "Карта",
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
"offline": "Отключено",
|
"offline": "Отключено",
|
||||||
"noMessages": "Нет сообщений",
|
"noMessages": "Нет сообщений",
|
||||||
"messagePlaceholder": "Сообщение...",
|
"messagePlaceholder": "Сообщение...",
|
||||||
"chats": "Чаты",
|
"chats": "Чаты по объявлениям",
|
||||||
"loginToSee": "Войдите, чтобы видеть чаты",
|
"loginToSee": "Войдите, чтобы видеть чаты",
|
||||||
"loading": "Загрузка...",
|
"loading": "Загрузка...",
|
||||||
"noChats": "У вас пока нет чатов",
|
"noChats": "У вас пока нет чатов",
|
||||||
@@ -390,5 +390,8 @@
|
|||||||
"favorites": {
|
"favorites": {
|
||||||
"toggleError": "Не удалось изменить статус избранного",
|
"toggleError": "Не удалось изменить статус избранного",
|
||||||
"loadError": "Не удалось загрузить избранное"
|
"loadError": "Не удалось загрузить избранное"
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"backToListings": "Назад к объявлениям"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ export default function MainFeed() {
|
|||||||
const [favoritesLoading, setFavoritesLoading] = useState(false);
|
const [favoritesLoading, setFavoritesLoading] = useState(false);
|
||||||
|
|
||||||
const feedRef = useRef(null);
|
const feedRef = useRef(null);
|
||||||
|
// Состояние ленты перед переходом на карту — чтобы вернуть юзера на то же место
|
||||||
|
const feedStateRef = useRef({ scrollTop: 0, url: "/feed", activeNav: "home" });
|
||||||
|
const restoreScrollRef = useRef(false);
|
||||||
|
|
||||||
const isSearchView = new URLSearchParams(location.search).get("view") === "search";
|
const isSearchView = new URLSearchParams(location.search).get("view") === "search";
|
||||||
const isMapView = new URLSearchParams(location.search).get("view") === "map";
|
const isMapView = new URLSearchParams(location.search).get("view") === "map";
|
||||||
@@ -230,18 +233,44 @@ export default function MainFeed() {
|
|||||||
}, [isSearchView, isMapView, navigate]);
|
}, [isSearchView, isMapView, navigate]);
|
||||||
|
|
||||||
// Карта с объявлениями
|
// Карта с объявлениями
|
||||||
|
const saveFeedState = useCallback(() => {
|
||||||
|
feedStateRef.current = {
|
||||||
|
scrollTop: feedRef.current?.scrollTop ?? 0,
|
||||||
|
url: `${location.pathname}${location.search}`,
|
||||||
|
activeNav,
|
||||||
|
};
|
||||||
|
restoreScrollRef.current = true;
|
||||||
|
}, [location.pathname, location.search, activeNav]);
|
||||||
|
|
||||||
const handleMapClick = useCallback(() => {
|
const handleMapClick = useCallback(() => {
|
||||||
|
saveFeedState();
|
||||||
setActiveNav("map");
|
setActiveNav("map");
|
||||||
if (!isMapView) navigate("/feed?view=map");
|
if (!isMapView) navigate("/feed?view=map");
|
||||||
feedRef.current?.scrollTo({ top: 0, behavior: "smooth" });
|
}, [isMapView, navigate, saveFeedState]);
|
||||||
}, [isMapView, navigate]);
|
|
||||||
|
|
||||||
const handleMapPostClick = useCallback((postId) => {
|
const handleMapPostClick = useCallback((postId) => {
|
||||||
modals.open("postDetail", { postId });
|
modals.open("postDetail", { postId });
|
||||||
}, [modals]);
|
}, [modals]);
|
||||||
|
|
||||||
|
// Возврат с карты на ленту — на то же место, где юзер был до открытия карты
|
||||||
|
const handleMapBack = useCallback(() => {
|
||||||
|
const { url, activeNav: prevNav } = feedStateRef.current;
|
||||||
|
setActiveNav(prevNav || "home");
|
||||||
|
navigate(url || "/feed", { replace: true });
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
// Восстанавливаем прокрутку ленты после возврата с карты
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMapView || !restoreScrollRef.current) return;
|
||||||
|
const el = feedRef.current;
|
||||||
|
if (el) el.scrollTop = feedStateRef.current.scrollTop;
|
||||||
|
restoreScrollRef.current = false;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isMapView, feed.ads, cityFeed.cityAds, feed.loading, cityFeed.cityLoading]);
|
||||||
|
|
||||||
// Клик по локации на карточке — карта с фокусом на этом объявлении
|
// Клик по локации на карточке — карта с фокусом на этом объявлении
|
||||||
const handleLocationClick = useCallback((ad) => {
|
const handleLocationClick = useCallback((ad) => {
|
||||||
|
saveFeedState();
|
||||||
const params = new URLSearchParams({ view: "map", post: ad.id });
|
const params = new URLSearchParams({ view: "map", post: ad.id });
|
||||||
if (ad.latitude != null && ad.longitude != null) {
|
if (ad.latitude != null && ad.longitude != null) {
|
||||||
params.set("lat", ad.latitude);
|
params.set("lat", ad.latitude);
|
||||||
@@ -249,8 +278,7 @@ export default function MainFeed() {
|
|||||||
}
|
}
|
||||||
setActiveNav("map");
|
setActiveNav("map");
|
||||||
navigate(`/feed?${params.toString()}`);
|
navigate(`/feed?${params.toString()}`);
|
||||||
feedRef.current?.scrollTo({ top: 0, behavior: "smooth" });
|
}, [navigate, saveFeedState]);
|
||||||
}, [navigate]);
|
|
||||||
|
|
||||||
const handleProfileClick = useCallback(() => {
|
const handleProfileClick = useCallback(() => {
|
||||||
if (!isAuthenticated) modals.open("auth");
|
if (!isAuthenticated) modals.open("auth");
|
||||||
@@ -332,7 +360,13 @@ export default function MainFeed() {
|
|||||||
|
|
||||||
<div className="f-wrap">
|
<div className="f-wrap">
|
||||||
{isMapView ? (
|
{isMapView ? (
|
||||||
<MapView focusPostId={focusPostId} focusLat={focusLat} focusLng={focusLng} onPostClick={handleMapPostClick} />
|
<MapView
|
||||||
|
focusPostId={focusPostId}
|
||||||
|
focusLat={focusLat}
|
||||||
|
focusLng={focusLng}
|
||||||
|
onPostClick={handleMapPostClick}
|
||||||
|
onBack={handleMapBack}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="f-feed" ref={feedRef}>
|
<div className="f-feed" ref={feedRef}>
|
||||||
|
|||||||
@@ -456,6 +456,50 @@
|
|||||||
.f-mapview-geo-info { color: var(--muted); }
|
.f-mapview-geo-info { color: var(--muted); }
|
||||||
.f-mapview-geo-warn { color: var(--err); border-color: rgba(237,73,86,.35); }
|
.f-mapview-geo-warn { color: var(--err); border-color: rgba(237,73,86,.35); }
|
||||||
|
|
||||||
|
/* Back-to-feed button on map view */
|
||||||
|
.f-mapview-back {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px; left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 8;
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 12px 22px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, .12);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(20, 20, 20, .55);
|
||||||
|
backdrop-filter: blur(30px) saturate(1.6); -webkit-backdrop-filter: blur(30px) saturate(1.6);
|
||||||
|
color: #fff;
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: .92rem; font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,.3), inset 0 0.5px 0 rgba(255,255,255,.08);
|
||||||
|
transition: background .15s, border-color .15s, transform .15s, box-shadow .15s;
|
||||||
|
}
|
||||||
|
.f-mapview-back-icon { width: 18px; height: 18px; flex-shrink: 0; }
|
||||||
|
|
||||||
|
:root:not([data-theme="dark"]) .f-mapview-back {
|
||||||
|
background: rgba(255, 255, 255, .6);
|
||||||
|
border-color: rgba(0, 0, 0, .08);
|
||||||
|
color: var(--text-primary);
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,.08), inset 0 0.5px 0 rgba(255,255,255,.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.f-mapview-back:hover {
|
||||||
|
background: rgba(255, 255, 255, .22);
|
||||||
|
border-color: rgba(255, 255, 255, .28);
|
||||||
|
transform: translateX(-50%) translateY(-2px);
|
||||||
|
box-shadow: 0 10px 36px rgba(0,0,0,.35), inset 0 0.5px 0 rgba(255,255,255,.12);
|
||||||
|
}
|
||||||
|
:root:not([data-theme="dark"]) .f-mapview-back:hover {
|
||||||
|
background: rgba(255, 255, 255, .85);
|
||||||
|
border-color: rgba(0, 0, 0, .14);
|
||||||
|
}
|
||||||
|
.f-mapview-back:active {
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,.2), inset 0 0.5px 0 rgba(255,255,255,.08);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Feed Wrap + Scroll ── */
|
/* ── Feed Wrap + Scroll ── */
|
||||||
.f-wrap { display: flex; flex: 1; overflow: hidden; min-height: 0; }
|
.f-wrap { display: flex; flex: 1; overflow: hidden; min-height: 0; }
|
||||||
|
|
||||||
@@ -481,6 +525,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
|
.f-mapview-back {
|
||||||
|
/* Поднимаем кнопку выше плавающей нижней навигации */
|
||||||
|
bottom: calc(84px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
.f-feed {
|
.f-feed {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -522,21 +522,33 @@ select.f-m-field::selection { background: transparent; }
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: min(90vh, 820px);
|
||||||
}
|
}
|
||||||
.f-post-detail-modal .f-card {
|
.f-post-detail-modal .f-card {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
|
.f-post-detail-modal .f-card::-webkit-scrollbar { width: 4px; }
|
||||||
|
.f-post-detail-modal .f-card::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
.f-post-detail-modal .f-card::-webkit-scrollbar-thumb { background: var(--muted2); border-radius: 2px; }
|
||||||
.f-post-detail-modal .f-card-head {
|
.f-post-detail-modal .f-card-head {
|
||||||
border-radius: 24px 24px 0 0;
|
border-radius: 24px 24px 0 0;
|
||||||
}
|
flex-shrink: 0;
|
||||||
.f-post-detail-modal .f-card-head {
|
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
}
|
}
|
||||||
.f-post-detail-modal .f-card-img {
|
.f-post-detail-modal .f-card-img {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.f-post-detail-modal .f-card-img img {
|
.f-post-detail-modal .f-card-img img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -546,8 +558,25 @@ select.f-m-field::selection { background: transparent; }
|
|||||||
.f-post-detail-modal .f-card-caption {
|
.f-post-detail-modal .f-card-caption {
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
}
|
}
|
||||||
|
.f-post-detail-modal .f-card-caption-text {
|
||||||
|
-webkit-line-clamp: unset;
|
||||||
|
line-clamp: unset;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
.f-post-detail-modal .f-card-foot {
|
.f-post-detail-modal .f-card-foot {
|
||||||
padding: 8px 14px;
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 16px calc(12px + env(safe-area-inset-bottom, 0px));
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
box-shadow: 0 -6px 20px rgba(0, 0, 0, .06);
|
||||||
}
|
}
|
||||||
.f-app.f-map-view .f-card-foot .f-card-phone {
|
.f-app.f-map-view .f-card-foot .f-card-phone {
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user