fix bug and errors
This commit is contained in:
@@ -106,7 +106,7 @@ function LoginFormInline({ saveAuth, onClose, onSwitch }) {
|
||||
|
||||
<p style={{textAlign: "center", marginTop: 14, color: "var(--muted)", fontSize: ".85rem"}}>
|
||||
{t('auth.noAccount')}{" "}
|
||||
<button onClick={onSwitch}
|
||||
<button type="button" onClick={onSwitch}
|
||||
style={{background: "none", border: "none", color: "var(--teal)", cursor: "pointer", fontWeight: 700, fontSize: ".85rem", fontFamily: "var(--font)",}}
|
||||
>
|
||||
{t('auth.register')}
|
||||
@@ -224,7 +224,7 @@ function RegisterFormInline({ saveAuth, onClose, onSwitch }) {
|
||||
|
||||
<p style={{textAlign: "center", marginTop: 14, color: "var(--muted)", fontSize: ".85rem"}}>
|
||||
{t('auth.hasAccount')}{" "}
|
||||
<button onClick={onSwitch}
|
||||
<button type="button" onClick={onSwitch}
|
||||
style={{background: "none", border: "none", color: "var(--teal)", cursor: "pointer", fontWeight: 700, fontSize: ".85rem", fontFamily: "var(--font)",}}>
|
||||
{t('auth.loginLink')}
|
||||
</button>
|
||||
|
||||
@@ -2,6 +2,7 @@ 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";
|
||||
import { whenYmapsReady } from "../utils/ymapsApi";
|
||||
|
||||
const MARKER_COLORS = {
|
||||
LOST: "#ff3b30",
|
||||
@@ -118,6 +119,10 @@ export default function MapView({
|
||||
onBack,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
// tRef нужен, чтобы читать перевод из эффектов, не добавляя t в их deps:
|
||||
// иначе смена языка пересоздавала бы карту и все маркеры с нуля.
|
||||
const tRef = useRef(t);
|
||||
tRef.current = t;
|
||||
const mapRef = useRef(null);
|
||||
const ymapRef = useRef(null);
|
||||
const userMarkerRef = useRef(null);
|
||||
@@ -319,9 +324,20 @@ export default function MapView({
|
||||
}
|
||||
};
|
||||
|
||||
if (window.ymaps?.ready) {
|
||||
window.ymaps.ready(init);
|
||||
}
|
||||
whenYmapsReady()
|
||||
.then((ymaps) => {
|
||||
if (destroyed) return;
|
||||
ymaps.ready(init);
|
||||
})
|
||||
.catch((e) => {
|
||||
// API карт не подгрузился (заблокирован/офлайн/медленный CDN).
|
||||
// Без этой ветки пользователь смотрел бы в вечный спиннер без ошибки.
|
||||
if (!destroyed) {
|
||||
setError(tRef.current("post.mapLoadFailed"));
|
||||
setLoading(false);
|
||||
}
|
||||
console.warn("Yandex Maps API failed to load:", e);
|
||||
});
|
||||
|
||||
return () => {
|
||||
destroyed = true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/components/Toast.jsx
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
/**
|
||||
* Toast — всплывающее сообщение об ошибке/успехе, автоматически скрывается.
|
||||
@@ -10,10 +10,16 @@ import { useEffect } from "react";
|
||||
* @param {number} [duration=4000] — время жизни в мс
|
||||
*/
|
||||
export default function Toast({ message, type = "error", onDismiss, duration = 4000 }) {
|
||||
// onDismiss почти всегда — свежий инлайн-стрелки. Если держать его в
|
||||
// зависимостях useEffect, любой ре-рендер родителя (сообщения чата, поллинг
|
||||
// рекламы) перезапускал бы отсчёт и тост висел бы бесконечно.
|
||||
const dismissRef = useRef(onDismiss);
|
||||
dismissRef.current = onDismiss;
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(onDismiss, duration);
|
||||
const timer = setTimeout(() => dismissRef.current?.(), duration);
|
||||
return () => clearTimeout(timer);
|
||||
}, [onDismiss, duration]);
|
||||
}, [duration]);
|
||||
|
||||
if (!message) return null;
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* Отдельный блюр-слой поверх ленты, независим от FeedTopBar.
|
||||
* Прижат к верху общего контейнера, высота подстраивается под
|
||||
* реальную высоту topBar (включая safe-area на iOS).
|
||||
*/
|
||||
export default function FeedBlurMask({ topBarRef }) {
|
||||
const [height, setHeight] = useState(56);
|
||||
|
||||
useEffect(() => {
|
||||
const el = topBarRef?.current;
|
||||
if (!el) return;
|
||||
const update = () => setHeight(el.getBoundingClientRect().height || 56);
|
||||
update();
|
||||
const ro = new ResizeObserver(update);
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [topBarRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="f-feed-blur-mask"
|
||||
style={{ height }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -35,6 +35,9 @@ export default function PullToRefresh({ containerRef, onRefresh, disabled = fals
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
el.classList.toggle("f-ptr-active", visible);
|
||||
// Размонтирование с visible=true иначе навсегда оставило бы маску
|
||||
// затухания и блокировку pointer-events на контейнере ленты.
|
||||
return () => el.classList.remove("f-ptr-active");
|
||||
}, [visible, containerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export default function TypeFilterBar({ value, onChange, t }) {
|
||||
const options = [
|
||||
{ key: "loss", label: t('feed.filterLoss') },
|
||||
{ key: null, label: t('feed.filterAll') },
|
||||
{ key: "found", label: t('feed.filterFound') },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="f-filter-bar">
|
||||
{options.map(opt => (
|
||||
<button
|
||||
key={opt.key ?? "all"}
|
||||
className={`f-filter-btn${value === opt.key ? " active" : ""}`}
|
||||
onClick={() => onChange(opt.key)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export default function HeartIcon({filled}) {
|
||||
return (
|
||||
<svg width={14} height={14} viewBox="0 0 24 24"
|
||||
fill={filled ? "#ff6b6b" : "none"} stroke="#ff6b6b" strokeWidth={2}
|
||||
strokeLinecap="round">
|
||||
<path
|
||||
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import magnifier from "../../assets/magnifier.png";
|
||||
|
||||
export default function LogoIcon() {
|
||||
return (
|
||||
<img
|
||||
src={magnifier}
|
||||
alt="logo"
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export default function MapIcon() {
|
||||
return (
|
||||
<svg width={14} height={14} viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth={2} strokeLinecap="round">
|
||||
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/>
|
||||
<line x1="8" y1="2" x2="8" y2="18"/>
|
||||
<line x1="16" y1="6" x2="16" y2="22"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user