45
src/App.js
45
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 (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<UnreadListener />
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/feed" element={<MainFeed />} />
|
||||
<Route path="/reviews" element={<ReviewsPage />} />
|
||||
<Route path="/chats" element={<ChatListPage />} />
|
||||
<Route path="/chats/:chatId" element={<ChatPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<AuthProvider>
|
||||
<UnreadListener />
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/feed" element={<MainFeed />} />
|
||||
<Route path="/reviews" element={<ReviewsPage />} />
|
||||
<Route path="/chats" element={<ChatListPage />} />
|
||||
<Route path="/chats/:chatId" element={<ChatPage />} />
|
||||
<Route path="*" element={<LandingPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
@@ -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 (
|
||||
<UnreadContext.Provider value={{ unreadMap, totalUnread, setChatUnread, clearChatUnread, updateFromChats }}>
|
||||
<UnreadContext.Provider value={{ unreadMap, totalUnread, setChatUnread, clearChatUnread, updateFromChats, resetUnread }}>
|
||||
{children}
|
||||
</UnreadContext.Provider>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
/**
|
||||
* Форматирует строку в маску +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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user