From 2b9cb62c7264ad36ccb2f0282078b75d722f5962 Mon Sep 17 00:00:00 2001 From: SlimusMinus Date: Wed, 12 Aug 2026 23:28:35 +0300 Subject: [PATCH] fixed code v3 --- src/App.js | 47 ++++++++++++++++++++++++----------- src/context/UnreadContext.jsx | 14 ++++++++++- src/utils/compressImage.js | 27 ++++++++++++++------ src/utils/formatPhone.js | 8 ++++-- src/utils/validatePostForm.js | 11 ++++++-- 5 files changed, 79 insertions(+), 28 deletions(-) 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