added button back from map and fix post on map

This commit is contained in:
SlimusMinus
2026-09-07 01:32:22 +03:00
parent 3299df14c6
commit f0a7327a1a
6 changed files with 140 additions and 11 deletions

View File

@@ -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>
); );
} }

View File

@@ -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"
} }
} }

View File

@@ -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": "Назад к объявлениям"
} }
} }

View File

@@ -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}>

View File

@@ -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;

View File

@@ -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;