diff --git a/src/App.js b/src/App.js
index ba1f7be..3257dff 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,7 +1,10 @@
+// src/App.jsx
+import { useEffect, useRef } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { ThemeProvider } from './context/ThemeContext';
import { useAuthContext } from './context/AuthContext';
+import { useUnread } from './context/UnreadContext';
import { useUnreadSocket } from './hooks/useUnreadSocket';
import LandingPage from './pages/NakhodkaLanding';
import MainFeed from './pages/MainFeed';
@@ -12,6 +15,19 @@ import './styles/chat.css';
function UnreadListener() {
const { user } = useAuthContext();
+ const { resetUnread } = useUnread();
+ const prevUserIdRef = useRef(user?.id);
+
+ // Сбрасываем накопленный unreadMap при смене пользователя (логин под другим
+ // аккаунтом или логаут → user становится null) — иначе счётчики непрочитанных
+ // от предыдущей сессии останутся висеть в бейдже до первого WS-обновления.
+ useEffect(() => {
+ if (prevUserIdRef.current !== user?.id) {
+ resetUnread();
+ prevUserIdRef.current = user?.id;
+ }
+ }, [user?.id, resetUnread]);
+
useUnreadSocket(user?.id);
return null;
}
@@ -19,20 +35,21 @@ function UnreadListener() {
function App() {
return (
-
-
-
-
- } />
- } />
- } />
- } />
- } />
-
-
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
- );
- }
-
- export default App;
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/src/context/UnreadContext.jsx b/src/context/UnreadContext.jsx
index f4fe302..a437ad7 100644
--- a/src/context/UnreadContext.jsx
+++ b/src/context/UnreadContext.jsx
@@ -1,3 +1,4 @@
+// src/context/UnreadContext.jsx
import { createContext, useContext, useState, useCallback, useMemo } from 'react';
const UnreadContext = createContext(null);
@@ -46,13 +47,24 @@ export function UnreadProvider({ children }) {
});
}, []);
+ /**
+ * Полностью сбрасывает unreadMap — вызывается при смене пользователя
+ * (логин/логаут), чтобы счётчики предыдущей сессии не "протекали"
+ * в новую. UnreadProvider не вложен внутрь AuthProvider, поэтому
+ * реагировать на logout() напрямую не может — это делает компонент
+ * ниже по дереву, имеющий доступ к обоим контекстам (см. UnreadListener в App.jsx).
+ */
+ const resetUnread = useCallback(() => {
+ setUnreadMap({});
+ }, []);
+
const totalUnread = useMemo(() =>
Object.values(unreadMap).reduce((sum, c) => sum + c, 0),
[unreadMap]
);
return (
-
+
{children}
);
diff --git a/src/utils/compressImage.js b/src/utils/compressImage.js
index 60fa95b..5bcfc29 100644
--- a/src/utils/compressImage.js
+++ b/src/utils/compressImage.js
@@ -23,16 +23,27 @@ export function compressImage(file, maxW = 1200, quality = 0.82) {
canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob(
- blob =>
- blob
- ? resolve(new File([blob], file.name, { type: "image/jpeg" }))
- : reject(new Error("canvas.toBlob вернул null")),
- "image/jpeg",
- quality
+ blob => {
+ if (!blob) {
+ reject(new Error("canvas.toBlob вернул null"));
+ return;
+ }
+ // Имя приводим к .jpg — результат всегда JPEG, независимо от
+ // исходного расширения (png/webp/etc), иначе имя файла не
+ // соответствует его реальному содержимому.
+ const jpegName = file.name.replace(/\.[^.]+$/, "") + ".jpg";
+ resolve(new File([blob], jpegName, { type: "image/jpeg" }));
+ },
+ "image/jpeg",
+ quality
);
};
- img.onerror = reject;
+ img.onerror = () => {
+ URL.revokeObjectURL(url);
+ reject(new Error("Не удалось загрузить изображение"));
+ };
+
img.src = url;
});
-}
+}
\ No newline at end of file
diff --git a/src/utils/formatPhone.js b/src/utils/formatPhone.js
index 4139447..d7f102b 100644
--- a/src/utils/formatPhone.js
+++ b/src/utils/formatPhone.js
@@ -1,17 +1,21 @@
/**
* Форматирует строку в маску +7 (___) ___-__-__.
* Принимает любой ввод — чистит всё кроме цифр.
+ * Поддерживает ввод с "7" или "8" в начале (стандартный
+ * российский формат телефонного номера).
*
* @param {string} val — сырое значение из input
* @returns {string} — отформатированная строка
*/
export function formatPhone(val) {
const digits = val.replace(/\D/g, "");
- const local = digits.startsWith("7") ? digits.slice(1) : digits;
+ const local = digits.startsWith("7") || digits.startsWith("8")
+ ? digits.slice(1)
+ : digits;
let result = "+7";
if (local.length > 0) result += " (" + local.slice(0, 3);
if (local.length >= 3) result += ") " + local.slice(3, 6);
if (local.length >= 6) result += "-" + local.slice(6, 8);
if (local.length >= 8) result += "-" + local.slice(8, 10);
return result;
-}
+}
\ No newline at end of file
diff --git a/src/utils/validatePostForm.js b/src/utils/validatePostForm.js
index 864b04f..599d145 100644
--- a/src/utils/validatePostForm.js
+++ b/src/utils/validatePostForm.js
@@ -5,7 +5,14 @@ export function validatePostForm(form) {
if (!form.title.trim()) errors.title = i18n.t('validation.titleRequired');
if (!form.description.trim()) errors.description = i18n.t('validation.descriptionRequired');
if (!form.category) errors.category = i18n.t('validation.categoryRequired');
- if (!form.phone.trim()) errors.phone = i18n.t('validation.phoneRequired');
+
+ // Телефон обязателен для объявления (в отличие от профиля) — проверяем
+ // количество цифр, а не просто пустоту, т.к. form.phone форматируется
+ // через formatPhone и всегда содержит минимум "+7".
+ if (form.phone.replace(/\D/g, "").length < 11) {
+ errors.phone = i18n.t('validation.phoneRequired');
+ }
+
if (!form.rulesAccepted) errors.rulesAccepted = i18n.t('validation.rulesRequired');
return errors;
-}
+}
\ No newline at end of file