Merge pull request #40 from SlimusMinus/fix-code_v3

fixed code v3
This commit is contained in:
SlimusMinus
2026-08-12 23:30:02 +03:00
committed by GitHub
5 changed files with 79 additions and 28 deletions

View File

@@ -1,7 +1,10 @@
// src/App.jsx
import { useEffect, useRef } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext'; import { AuthProvider } from './context/AuthContext';
import { ThemeProvider } from './context/ThemeContext'; import { ThemeProvider } from './context/ThemeContext';
import { useAuthContext } from './context/AuthContext'; import { useAuthContext } from './context/AuthContext';
import { useUnread } from './context/UnreadContext';
import { useUnreadSocket } from './hooks/useUnreadSocket'; import { useUnreadSocket } from './hooks/useUnreadSocket';
import LandingPage from './pages/NakhodkaLanding'; import LandingPage from './pages/NakhodkaLanding';
import MainFeed from './pages/MainFeed'; import MainFeed from './pages/MainFeed';
@@ -12,6 +15,19 @@ import './styles/chat.css';
function UnreadListener() { function UnreadListener() {
const { user } = useAuthContext(); 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); useUnreadSocket(user?.id);
return null; return null;
} }
@@ -19,20 +35,21 @@ function UnreadListener() {
function App() { function App() {
return ( return (
<ThemeProvider> <ThemeProvider>
<AuthProvider> <AuthProvider>
<UnreadListener /> <UnreadListener />
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route path="/" element={<LandingPage />} /> <Route path="/" element={<LandingPage />} />
<Route path="/feed" element={<MainFeed />} /> <Route path="/feed" element={<MainFeed />} />
<Route path="/reviews" element={<ReviewsPage />} /> <Route path="/reviews" element={<ReviewsPage />} />
<Route path="/chats" element={<ChatListPage />} /> <Route path="/chats" element={<ChatListPage />} />
<Route path="/chats/:chatId" element={<ChatPage />} /> <Route path="/chats/:chatId" element={<ChatPage />} />
</Routes> <Route path="*" element={<LandingPage />} />
</BrowserRouter> </Routes>
</BrowserRouter>
</AuthProvider> </AuthProvider>
</ThemeProvider> </ThemeProvider>
); );
} }
export default App; export default App;

View File

@@ -1,3 +1,4 @@
// src/context/UnreadContext.jsx
import { createContext, useContext, useState, useCallback, useMemo } from 'react'; import { createContext, useContext, useState, useCallback, useMemo } from 'react';
const UnreadContext = createContext(null); 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(() => const totalUnread = useMemo(() =>
Object.values(unreadMap).reduce((sum, c) => sum + c, 0), Object.values(unreadMap).reduce((sum, c) => sum + c, 0),
[unreadMap] [unreadMap]
); );
return ( return (
<UnreadContext.Provider value={{ unreadMap, totalUnread, setChatUnread, clearChatUnread, updateFromChats }}> <UnreadContext.Provider value={{ unreadMap, totalUnread, setChatUnread, clearChatUnread, updateFromChats, resetUnread }}>
{children} {children}
</UnreadContext.Provider> </UnreadContext.Provider>
); );

View File

@@ -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.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob( canvas.toBlob(
blob => blob => {
blob if (!blob) {
? resolve(new File([blob], file.name, { type: "image/jpeg" })) reject(new Error("canvas.toBlob вернул null"));
: reject(new Error("canvas.toBlob вернул null")), return;
"image/jpeg", }
quality // Имя приводим к .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; img.src = url;
}); });
} }

View File

@@ -1,17 +1,21 @@
/** /**
* Форматирует строку в маску +7 (___) ___-__-__. * Форматирует строку в маску +7 (___) ___-__-__.
* Принимает любой ввод — чистит всё кроме цифр. * Принимает любой ввод — чистит всё кроме цифр.
* Поддерживает ввод с "7" или "8" в начале (стандартный
* российский формат телефонного номера).
* *
* @param {string} val — сырое значение из input * @param {string} val — сырое значение из input
* @returns {string} — отформатированная строка * @returns {string} — отформатированная строка
*/ */
export function formatPhone(val) { export function formatPhone(val) {
const digits = val.replace(/\D/g, ""); 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"; let result = "+7";
if (local.length > 0) result += " (" + local.slice(0, 3); if (local.length > 0) result += " (" + local.slice(0, 3);
if (local.length >= 3) result += ") " + local.slice(3, 6); if (local.length >= 3) result += ") " + local.slice(3, 6);
if (local.length >= 6) result += "-" + local.slice(6, 8); if (local.length >= 6) result += "-" + local.slice(6, 8);
if (local.length >= 8) result += "-" + local.slice(8, 10); if (local.length >= 8) result += "-" + local.slice(8, 10);
return result; return result;
} }

View File

@@ -5,7 +5,14 @@ export function validatePostForm(form) {
if (!form.title.trim()) errors.title = i18n.t('validation.titleRequired'); if (!form.title.trim()) errors.title = i18n.t('validation.titleRequired');
if (!form.description.trim()) errors.description = i18n.t('validation.descriptionRequired'); if (!form.description.trim()) errors.description = i18n.t('validation.descriptionRequired');
if (!form.category) errors.category = i18n.t('validation.categoryRequired'); 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'); if (!form.rulesAccepted) errors.rulesAccepted = i18n.t('validation.rulesRequired');
return errors; return errors;
} }