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:
SlimusMinus
2026-09-07 01:33:24 +03:00
committed by GitHub
6 changed files with 140 additions and 11 deletions

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { API_URL, getImageUrl } from "../config/authConstants";
import { useGeolocation, GEO_STATUS } from "../hooks/useGeolocation";
@@ -40,7 +41,8 @@ function createMarkerLayout(color, imageUrl) {
const FOCUS_ZOOM = 16;
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 ymapRef = useRef(null);
const userMarkerRef = useRef(null);
@@ -215,6 +217,15 @@ export default function MapView({ focusPostId, focusLat, focusLng, onPostClick }
{geoStatus === GEO_STATUS.UNSUPPORTED && (
<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>
);
}

View File

@@ -389,5 +389,8 @@
"favorites": {
"toggleError": "Failed to change the favorite status.",
"loadError": "Failed to load favorites"
},
"map": {
"backToListings": "Back to listings"
}
}

View File

@@ -8,7 +8,7 @@
"favorites": "Избранное",
"items": "Вещи",
"pets": "Питомцы",
"chat": "Чат",
"chat": "Чаты по объявлениям",
"create": "Создать",
"search": "Поиск",
"map": "Карта",
@@ -179,7 +179,7 @@
"offline": "Отключено",
"noMessages": "Нет сообщений",
"messagePlaceholder": "Сообщение...",
"chats": "Чаты",
"chats": "Чаты по объявлениям",
"loginToSee": "Войдите, чтобы видеть чаты",
"loading": "Загрузка...",
"noChats": "У вас пока нет чатов",
@@ -390,5 +390,8 @@
"favorites": {
"toggleError": "Не удалось изменить статус избранного",
"loadError": "Не удалось загрузить избранное"
},
"map": {
"backToListings": "Назад к объявлениям"
}
}

View File

@@ -55,6 +55,9 @@ export default function MainFeed() {
const [favoritesLoading, setFavoritesLoading] = useState(false);
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 isMapView = new URLSearchParams(location.search).get("view") === "map";
@@ -230,18 +233,44 @@ export default function MainFeed() {
}, [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(() => {
saveFeedState();
setActiveNav("map");
if (!isMapView) navigate("/feed?view=map");
feedRef.current?.scrollTo({ top: 0, behavior: "smooth" });
}, [isMapView, navigate]);
}, [isMapView, navigate, saveFeedState]);
const handleMapPostClick = useCallback((postId) => {
modals.open("postDetail", { postId });
}, [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) => {
saveFeedState();
const params = new URLSearchParams({ view: "map", post: ad.id });
if (ad.latitude != null && ad.longitude != null) {
params.set("lat", ad.latitude);
@@ -249,8 +278,7 @@ export default function MainFeed() {
}
setActiveNav("map");
navigate(`/feed?${params.toString()}`);
feedRef.current?.scrollTo({ top: 0, behavior: "smooth" });
}, [navigate]);
}, [navigate, saveFeedState]);
const handleProfileClick = useCallback(() => {
if (!isAuthenticated) modals.open("auth");
@@ -332,7 +360,13 @@ export default function MainFeed() {
<div className="f-wrap">
{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}>

View File

@@ -456,6 +456,50 @@
.f-mapview-geo-info { color: var(--muted); }
.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 ── */
.f-wrap { display: flex; flex: 1; overflow: hidden; min-height: 0; }
@@ -481,6 +525,11 @@
}
@media (max-width: 900px) {
.f-mapview-back {
/* Поднимаем кнопку выше плавающей нижней навигации */
bottom: calc(84px + env(safe-area-inset-bottom));
}
.f-feed {
padding: 10px;
gap: 12px;

View File

@@ -522,21 +522,33 @@ select.f-m-field::selection { background: transparent; }
padding: 0;
overflow: hidden;
border-radius: 24px;
display: flex;
flex-direction: column;
max-height: min(90vh, 820px);
}
.f-post-detail-modal .f-card {
border: none;
border-radius: 24px;
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 {
border-radius: 24px 24px 0 0;
}
.f-post-detail-modal .f-card-head {
flex-shrink: 0;
padding: 10px 14px;
}
.f-post-detail-modal .f-card-img {
max-height: none;
overflow: visible;
flex-shrink: 0;
}
.f-post-detail-modal .f-card-img img {
width: 100%;
@@ -546,8 +558,25 @@ select.f-m-field::selection { background: transparent; }
.f-post-detail-modal .f-card-caption {
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 {
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 {
font-size: .75rem;